noble-secp256k1
Fastest JS implementation of secp256k1,
an elliptic curve that could be used for asymmetric encryption,
ECDH key agreement protocol and signature schemes.
Supports deterministic ECDSA from RFC6979 and Schnorr signatures from BIP0340.
Algorithmically resistant to timing attacks. Tested against thousands of vectors from tiny-secp256k1.
Check out the online demo and blog post: Learning fast elliptic-curve cryptography in JS
This library belongs to noble crypto
noble-crypto — high-security, easily auditable set of contained cryptographic libraries and tools.
- No dependencies, one small file
- Easily auditable TypeScript/JS code
- Supported in all major browsers and stable node.js versions
- All releases are signed with PGP keys
- Check out all libraries:
secp256k1,
ed25519,
bls12-381,
ripemd160
Usage
npm install noble-secp256k1
import * as secp from "noble-secp256k1";
(async () => {
const privateKey = "6b911fd37cdf5c81d4c0adb1ab7fa822ed253ab0ad9aa18d77257c88b29b718e";
const messageHash = "9c1185a5c5e9fc54612808977ee8f548b2258d31";
const publicKey = secp.getPublicKey(privateKey);
const signature = await secp.sign(messageHash, privateKey);
const isSigned = secp.verify(signature, messageHash, publicKey);
const rpub = secp.schnorr.getPublicKey(privateKey);
const rsignature = await secp.schnorr.sign(messageHash, privateKey);
const risSigned = await secp.schnorr.verify(rsignature, messageHash, rpub);
})();
Deno:
import * as secp from "https://deno.land/x/secp256k1/mod.ts";
const publicKey = secp.getPublicKey("6b911fd37cdf5c81d4c0adb1ab7fa822ed253ab0ad9aa18d77257c88b29b718e");
API
getPublicKey(privateKey)
function getPublicKey(privateKey: Uint8Array, isCompressed?: false): Uint8Array;
function getPublicKey(privateKey: string, isCompressed?: false): string;
function getPublicKey(privateKey: bigint): Uint8Array;
privateKey
will be used to generate public 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.
isCompressed
(default is false
) determines whether the output should contain y
coordinate of the point.
To get Point instance, use Point.fromPrivateKey(privateKey)
.
getSharedSecret(privateKeyA, publicKeyB)
function getSharedSecret(privateKeyA: Uint8Array, publicKeyB: Uint8Array): Uint8Array;
function getSharedSecret(privateKeyA: string, publicKeyB: string): string;
function getSharedSecret(privateKeyA: bigint, publicKeyB: Point): 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)
.
To speed-up the function massively by precomputing EC multiplications,
use getSharedSecret(privateKeyA, secp.utils.precompute(8, publicKeyB))
sign(hash, privateKey)
function sign(msgHash: Uint8Array, privateKey: Uint8Array, opts?: Options): Promise<Uint8Array>;
function sign(msgHash: string, privateKey: string, opts?: Options): Promise<string>;
function sign(msgHash: Uint8Array, privateKey: Uint8Array, opts?: Options): Promise<[Uint8Array | string, number]>;
Generates deterministic ECDSA signature as per RFC6979. Asynchronous, so use await
.
msgHash: Uint8Array | string
- message hash which would be signedprivateKey: Uint8Array | string | bigint
- private key which will sign the hashoptions?: Options
- optional object related to signature value and formatoptions?.recovered: boolean = false
- determines whether the recovered bit should be included in the result. In this case, the result would be an array of two items.options?.canonical: boolean = false
- determines whether a signature s
should be no more than 1/2 prime order- Returns DER encoded ECDSA signature, as hex uint8a / string and recovered bit if
options.recovered == true
.
verify(signature, hash, publicKey)
function verify(signature: Uint8Array, msgHash: Uint8Array, publicKey: Uint8Array): boolean
function verify(signature: string, msgHash: string, publicKey: string): boolean
signature: Uint8Array | string | { r: bigint, s: bigint }
- object returned by the sign
functionmsgHash: Uint8Array | string
- message hash that needs to be verifiedpublicKey: Uint8Array | string | Point
- e.g. that was generated from privateKey
by getPublicKey
- Returns
boolean
: true
if signature == hash
; otherwise false
recoverPublicKey(hash, signature, recovery)
export declare function recoverPublicKey(msgHash: string, signature: string, recovery: number): string | undefined;
export declare function recoverPublicKey(msgHash: Uint8Array, signature: Uint8Array, recovery: number): Uint8Array | undefined;
msgHash: Uint8Array | string
- message hash which would be signedsignature: Uint8Array | string | { r: bigint, s: bigint }
- object returned by the sign
functionrecovery: number
- recovery bit returned by sign
with recovered
option
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)
function schnorrGetPublicKey(privateKey: Uint8Array): Uint8Array;
function schnorrGetPublicKey(privateKey: string): string;
Returns 32-byte public key. Warning: it is incompatible with non-schnorr pubkey.
Specifically, its y coordinate may be flipped. See BIP0340 for clarification.
schnorr.sign(hash, privateKey)
function schnorrSign(msgHash: Uint8Array, privateKey: Uint8Array, auxilaryRandom?: Uint8Array): Promise<Uint8Array>;
function schnorrSign(msgHash: string, privateKey: string, auxilaryRandom?: string): Promise<string>;
Generates Schnorr signature as per BIP0340. Asynchronous, so use await
.
msgHash: Uint8Array | string
- message hash which would be signedprivateKey: Uint8Array | string | bigint
- private key which will sign the hashauxilaryRandom?: Uint8Array
— optional 32 random bytes. By default, the method gathers cryptogarphically secure random.- Returns Schnorr signature in Hex format.
schnorr.verify(signature, hash, publicKey)
function schnorrVerify(signature: Uint8Array | string, msgHash: Uint8Array | string, publicKey: Uint8Array | string): boolean
signature: Uint8Array | string | { r: bigint, s: bigint }
- object returned by the sign
functionmsgHash: Uint8Array | string
- message hash that needs to be verifiedpublicKey: Uint8Array | string | Point
- e.g. that was generated from privateKey
by getPublicKey
- Returns
boolean
: true
if signature == hash
; otherwise false
Point methods
Helpers
utils.generateRandomPrivateKey(): Uint8Array
Returns Uint8Array
of 32 cryptographically secure random bytes. You can use it as private key.
utils.precompute(W = 8, point = BASE_POINT): Point
Returns cached point which you can use to pass to getSharedSecret
or to #multiply
by it.
This is done by default, no need to run it unless you want to
disable precomputation or change window size.
We're doing scalar multiplication (used in getPublicKey etc) with
precomputed BASE_POINT values.
This slows down first getPublicKey() by milliseconds (see Speed section),
but allows to speed-up subsequent getPublicKey() calls up to 20x.
You may want to precompute values for your own point.
secp256k1.CURVE.P
secp256k1.CURVE.n
secp256k1.Point.BASE
secp256k1.Point {
constructor(x: bigint, y: bigint);
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;
multiply(scalar: bigint | Uint8Array): Point;
}
secp256k1.Signature {
constructor(r: bigint, s: bigint);
static fromHex(hex: Uint8Array | string);
toHex(): string;
}
Security
Noble is production-ready. Our goal is to have it audited by a good security expert.
We're using built-in JS BigInt
, which is "unsuitable for use in cryptography" as per official spec. This means that the lib is potentially vulnerable to timing attacks. 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 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 rootkits with every
npm install
. Our goal is to minimize this attack vector. - Nonetheless we've hardened implementation of koblitz curve multiplication to be algorithmically constant time.
Speed
Benchmarks measured with Apple M1.
getPublicKey(utils.randomPrivateKey()) x 5,605 ops/sec @ 178μs/op
sign x 3,915 ops/sec @ 255μs/op
verify x 820 ops/sec @ 1ms/op
recoverPublicKey x 436 ops/sec @ 2ms/op
getSharedSecret aka ecdh x 482 ops/sec @ 2ms/op
getSharedSecret (precomputed) x 6,152 ops/sec @ 162μs/op
schnorr.sign x 371 ops/sec @ 2ms/op
schnorr.verify x 469 ops/sec @ 2ms/op
Compare to other libraries (openssl
uses native bindings, not JS):
elliptic#getPublicKey x 1,940 ops/sec
sjcl#getPublicKey x 211 ops/sec
elliptic#sign x 1,808 ops/sec
sjcl#sign x 199 ops/sec
openssl#sign x 4,243 ops/sec
ecdsa#sign x 116 ops/sec
bip-schnorr#sign x 60 ops/sec
elliptic#verify x 812 ops/sec
sjcl#verify x 166 ops/sec
openssl#verify x 4,452 ops/sec
ecdsa#verify x 80 ops/sec
bip-schnorr#verify x 56 ops/sec
elliptic#ecdh x 971 ops/sec
Contributing
Check out a blog post about this library: Learning fast elliptic-curve cryptography in JS.
- Clone the repository.
npm install
to install build dependencies like TypeScriptnpm run compile
to compile TypeScript codenpm run test
to run jest on test/index.ts
Special thanks to Roman Koblov, who have helped to improve scalar multiplication speed.
License
MIT (c) Paul Miller (https://paulmillr.com), see LICENSE file.