Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch from noble-secp256k1 to noble-curves #74

Merged
merged 5 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8Array>;
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);
})();
```

Expand Down Expand Up @@ -448,9 +446,15 @@ you found another primitive that is missing.

## Upgrading

Version 1.0 changes from 0.1:
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.

**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
Expand Down
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "1.0.0",
"@noble/hashes": "1.3.0",
"@scure/bip32": "1.3.0",
"@scure/bip39": "1.2.0"
},
"browser": {
"crypto": false
Expand All @@ -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",
Expand All @@ -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": [
Expand Down
4 changes: 3 additions & 1 deletion src/aes.ts
Original file line number Diff line number Diff line change
@@ -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}`);
Expand Down
66 changes: 34 additions & 32 deletions src/secp256k1-compat.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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 {
Expand All @@ -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<Uint8Array> {
Expand All @@ -57,7 +59,7 @@ export async function createPrivateKey(): Promise<Uint8Array> {

export function privateKeyVerify(privateKey: Uint8Array): boolean {
assertBytes(privateKey, 32);
return secp.utils.isValidPrivateKey(privateKey);
return secp256k1.utils.isValidPrivateKey(privateKey);
}

export function publicKeyCreate(
Expand All @@ -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;
Expand All @@ -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);
}

Expand All @@ -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(
Expand All @@ -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));
}

Expand All @@ -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(
Expand Down Expand Up @@ -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));
}

Expand All @@ -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));
Expand All @@ -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));
Expand All @@ -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));
}

Expand Down Expand Up @@ -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
);
Expand All @@ -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()));
}

Expand Down Expand Up @@ -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)));
}
Expand All @@ -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");
Expand Down
24 changes: 1 addition & 23 deletions src/secp256k1.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1 @@
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();
};
export { secp256k1 } from "@noble/curves/secp256k1";
2 changes: 1 addition & 1 deletion test/test-vectors/hdkey.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
8 changes: 4 additions & 4 deletions test/test-vectors/secp256k1.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as secp from "../../src/secp256k1";
import { secp256k1 } from "../../src/secp256k1";
import { deepStrictEqual } from "./assert";

describe("secp256k1", () => {
Expand All @@ -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);
});
});