Fastest 5KB JS implementation of secp256k1 signatures and ECDH
noble-secp256k1
Fastest 5KB JS implementation of secp256k1 signatures & ECDH.
- ✍️ ECDSA
- ➰ Schnorr
- 🤝 Elliptic Curve Diffie-Hellman ECDH
- 🔒 Supports hedged signatures guarding against fault attacks
- 🪶 4.94KB (gzipped) - 10-25x smaller than similar libraries
898-byte version of the library is available for learning purposes in test/misc/1kb.min.js, it was created for the article Learning fast elliptic-curve cryptography.
This library belongs to noble cryptography
noble-cryptography — high-security, easily auditable set of contained cryptographic libraries and tools.
- Zero or minimal dependencies
- Highly readable TypeScript / JS code
- PGP-signed releases and transparent NPM builds
- All libraries:
- WASM version: awasm-noble
- Check out the homepage
Usage
npm install @noble/secp256k1
deno add jsr:@noble/secp256k1
We support all major platforms and runtimes. For React Native, additional polyfills are needed: see below.
import * as secp from '@noble/secp256k1';
(async () => {
const { secretKey, publicKey } = secp.keygen();
// const publicKey = secp.getPublicKey(secretKey);
const msg = new TextEncoder().encode('hello noble');
const sig = await secp.signAsync(msg, secretKey);
const isValid = await secp.verifyAsync(sig, msg, publicKey);
})();
// ECDH, key recovery (async () => { const alice = secp.keygen(); const bob = secp.keygen(); const shared = secp.getSharedSecret(alice.secretKey, bob.publicKey); const msg = new TextEncoder().encode('hello noble');
// recovery const sigr = await secp.signAsync(msg, alice.secretKey, { format: 'recovered' }); const publicKey2 = await secp.recoverPublicKeyAsync(sigr, msg); })();
// Schnorr signatures from BIP340 (async () => { const schnorr = secp.schnorr; const { secretKey, publicKey } = schnorr.keygen(); const msg = new TextEncoder().encode('hello noble'); const sig = await schnorr.signAsync(msg, secretKey); const isValid = await schnorr.verifyAsync(sig, msg, publicKey); })();
Enabling synchronous methods
Only async methods are available by default, to keep the library dependency-free. To enable sync methods:
npm install @noble/hashes
import * as secp from '@noble/secp256k1';
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
secp.hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
secp.hashes.sha256 = sha256;
React Native: polyfill getRandomValues and sha256
React Native does not provide secure getRandomValues by default. This can't be securely polyfilled from our end, so one will need a RN-specific compile-time dep.
import 'react-native-get-random-values';
import * as secp from '@noble/secp256k1';
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
secp.hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
secp.hashes.sha256 = sha256;
secp.hashes.hmacSha256Async = async (key, msg) => hmac(sha256, key, msg);
secp.hashes.sha256Async = async (msg) => sha256(msg);
API
There are 4 main methods, which accept Uint8Array-s:
keygen()getPublicKey(secretKey)sign(messageHash, secretKey)andsignAsync(messageHash, secretKey)verify(signature, messageHash, publicKey)andverifyAsync(signature, messageHash, publicKey)
keygen
import * as secp from '@noble/secp256k1';
(async () => {
const keys = secp.keygen();
const { secretKey, publicKey } = keys;
})();
getPublicKey
import * as secp from '@noble/secp256k1';
const secretKey = secp.utils.randomSecretKey();
const pubKey33b = secp.getPublicKey(secretKey);
// Variants const pubKey65b = secp.getPublicKey(secretKey, false); const pubKeyPoint = secp.Point.fromBytes(pubKey65b); const samePoint = pubKeyPoint.toBytes();
Generates 33-byte compressed (default) or 65-byte public key from 32-byte private key.
sign
import * as secp from '@noble/secp256k1';
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { keccak_256 } from '@noble/hashes/sha3.js';
secp.hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
secp.hashes.sha256 = sha256;
const { secretKey } = secp.keygen();
const msg = new TextEncoder().encode('hello noble');
const sig = secp.sign(msg, secretKey);
// async const sigB = await secp.signAsync(msg, secretKey);
// recovered, allows recoverPublicKey(sigR, msg) const sigR = secp.sign(msg, secretKey, { format: 'recovered' }); const sigH = secp.sign(keccak_256(msg), secretKey, { prehash: false }); // hedged sig const sigC = secp.sign(msg, secretKey, { extraEntropy: true }); const sigC2 = secp.sign(msg, secretKey, { extraEntropy: Uint8Array.from([0xca, 0xfe]) }); // malleable sig const sigD = secp.sign(msg, secretKey, { lowS: false });
Generates low-s deterministic-k RFC6979 ECDSA signature.
- Message will be hashed with sha256. If you want to use a different hash function,
{ prehash: false }.
extraEntropy: trueenables hedged signatures. They incorporate
- Default behavior
lowS: trueprohibits signatures which have (sig.s >= CURVE.n/2n) and is compatible with BTC/ETH. SettinglowS: falseallows to create malleable signatures, which is default openssl behavior. Non-malleable signatures can still be successfully verified in openssl.
verify
import * as secp from '@noble/secp256k1';
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { keccak_256 } from '@noble/hashes/sha3.js';
secp.hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
secp.hashes.sha256 = sha256;
const { secretKey, publicKey } = secp.keygen();
const msg = new TextEncoder().encode('hello noble');
const sig = secp.sign(msg, secretKey);
const isValid = secp.verify(sig, msg, publicKey);
const sigH = secp.sign(keccak_256(msg), secretKey, { prehash: false });
Verifies ECDSA signature.
- Message will be hashed with sha256. If you want to use a different hash function,
{ prehash: false }.
- Default behavior
lowS: trueprohibits malleable signatures which have (sig.s >= CURVE.n/2n) and
lowS: false allows to create signatures, which is default openssl behavior.
getSharedSecret
import * as secp from '@noble/secp256k1';
const alice = secp.keygen();
const bob = secp.keygen();
const shared33b = secp.getSharedSecret(alice.secretKey, bob.publicKey);
const shared65b = secp.getSharedSecret(bob.secretKey, alice.publicKey, false);
const sharedPoint = secp.Point.fromBytes(bob.publicKey).multiply(
secp.etc.secretKeyToScalar(alice.secretKey)
);
Computes ECDH (Elliptic Curve Diffie-Hellman) shared secret between key A and different key B.
recoverPublicKey
import * as secp from '@noble/secp256k1';
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { keccak_256 } from '@noble/hashes/sha3.js';
secp.hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
secp.hashes.sha256 = sha256;
const { secretKey, publicKey } = secp.keygen(); const msg = new TextEncoder().encode('hello noble'); const sigR = secp.sign(msg, secretKey, { format: 'recovered' }); const publicKey2 = secp.recoverPublicKey(sigR, msg);
const sigRH = secp.sign(keccak_256(msg), secretKey, { format: 'recovered', prehash: false }); const publicKeyH = secp.recoverPublicKey(sigRH, keccak_256(msg), { prehash: false });
Recover public key from Signature instance with recovery bit set.
schnorr
import * as secp from '@noble/secp256k1';
import { schnorr } from '@noble/secp256k1';
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
secp.hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
secp.hashes.sha256 = sha256;
const { secretKey, publicKey } = schnorr.keygen();
const msg = new TextEncoder().encode('hello noble');
const sig = schnorr.sign(msg, secretKey);
const isValid = schnorr.verify(sig, msg, publicKey);
Async methods work without extra setup:
import { schnorr } from '@noble/secp256k1';
const { secretKey, publicKey } = schnorr.keygen();
const msg = new TextEncoder().encode('hello noble');
const sigA = await schnorr.signAsync(msg, secretKey);
const isValidA = await schnorr.verifyAsync(sigA, msg, publicKey);
Schnorr signatures compliant with BIP340 are supported.
utils
A bunch of useful utilities are also exposed:
import * as secp from '@noble/secp256k1';
const { bytesToHex, hexToBytes, concatBytes, mod, invert, randomBytes } = secp.etc; const { isValidSecretKey, isValidPublicKey, randomSecretKey } = secp.utils; const { Point } = secp; console.log(Point.CURVE(), Point.BASE); /* class Point { static BASE: Point; static ZERO: Point; readonly X: bigint; readonly Y: bigint; readonly Z: bigint; constructor(X: bigint, Y: bigint, Z: bigint); static CURVE(): WeierstrassOpts<bigint>; static fromAffine(ap: AffinePoint): Point; static fromBytes(bytes: Bytes): Point; static fromHex(hex: string): Point; get x(): bigint; get y(): bigint; equals(other: Point): boolean; is0(): boolean; negate(): Point; double(): Point; add(other: Point): Point; subtract(other: Point): Point; multiply(n: bigint): Point; multiplyUnsafe(scalar: bigint): Point; toAffine(): AffinePoint; assertValidity(): Point; toBytes(isCompressed?: boolean): Bytes; toHex(isCompressed?: boolean): string; } */
Security
The module is production-ready.
We cross-test against sister project noble-curves, which was audited and provides improved security.
- The current version has not been independently audited. It is a rewrite of v1, which has been audited by cure53 in Apr 2021:
- It's being fuzzed in a separate repository
Constant-timeness
We're targetting algorithmic constant time. JIT-compiler and Garbage Collector make "constant time" extremely hard to achieve timing attack resistance in a scripting language. Which means _any other JS library can't have constant-timeness_. 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.
Supply chain security
- Commits are signed with PGP keys to prevent forgery. Be sure to verify the commit signatures
- Releases are made transparently through token-less GitHub CI and Trusted Publishing. Be sure to verify the provenance logs for authenticity.
- Rare releasing is practiced to minimize the need for re-audits by end-users.
- Dependencies are minimized and strictly pinned to reduce supply-chain risk.
- Dev dependencies are excluded from end-user installs; they’re only used for development and build steps.
- noble-hashes provides cryptographic hashing functionality
- jsbt is used for benchmarking / testing / build tooling and developed by the same author
- prettier, fast-check and typescript are used for code quality / test generation / ts compilation
Randomness
We rely on the built-in crypto.getRandomValues, which is considered a cryptographically secure PRNG.
Browsers have had weaknesses in the past - and could again - but implementing a userspace CSPRNG is even worse, as there’s no reliable userspace source of high-quality entropy.
Quantum computers
Cryptographically relevant quantum computer, if built, will allow to break elliptic curve cryptography (both ECDSA / EdDSA & ECDH) using Shor's algorithm.
Consider switching to newer / hybrid algorithms, such as SPHINCS+. They are available in noble-post-quantum.
NIST prohibits classical cryptography (RSA, DSA, ECDSA, ECDH) after 2035. Australian ASD prohibits it after 2030.
Upgrading
v2 to v3
v3 brings the package closer to noble-curves v2.
- Add Schnorr signatures
- Most methods now expect Uint8Array, string hex inputs are prohibited
- Add
keygen,keygenAsyncmethod - sign, verify: Switch to prehashed messages. Instead of
{prehash: false}
- sign, verify: Switch to Uint8Array signatures (format: 'compact') by default.
- verify: der format must be explicitly specified in
{format: 'der'}.
- verify: prohibit Signature-instance signature. User must now always do
signature.toBytes()
- Node v20.19 is now the minimum required version
- Various small changes for types
- etc: hashes are now set in
hashesobject. Also sha256 needs to be set now forprehash: true:
import { etc, hashes } from '@noble/secp256k1';
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
// before
etc.hmacSha256Sync = (key, ...messages) => hmac(sha256, key, etc.concatBytes(...messages));
etc.hmacSha256Async = (key, ...messages) => Promise.resolve(etc.hmacSha256Sync(key, ...messages));
// after
hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
hashes.sha256 = sha256;
hashes.hmacSha256Async = async (key, msg) => hmac(sha256, key, msg);
hashes.sha256Async = async (msg) => sha256(msg);
v1 to v2
v2 improves security and reduces attack surface. The goal of v2 is to provide minimum possible JS library which is safe and fast.
- Disable some features to ensure 4x smaller than v1, 5KB bundle size:
utils.precompute() for non-base point
getPublicKey
isCompressed to false: getPublicKey(priv, false)
sign
signAsync for async version
- now returns Signature instance with { r, s, recovery } properties
- canonical option was renamed to lowS
- recovered option has been removed because recovery bit is always returned now
- der option has been removed. There are 2 options:
1. Use compact encoding: fromCompact, toBytes, toCompactHex.
Compact encoding is simply a concatenation of 32-byte r and 32-byte s.
2. If you must use DER encoding, switch to noble-curves (see above).
verify
strict option was renamed to lowS
getSharedSecret
isCompressed to false: getSharedSecret(a, b, false)
recoverPublicKey(msg, sig, rec)was changed tosig.recoverPublicKey(msg)numbertype for private keys have been removed: usebigintinsteadPoint(2d xy) has been changed toProjectivePoint(3d xyz)utilswere split intoutils(same api as in noble-curves) and
etc (hmacSha256Sync and others)
Contributing & testing
npm install && npm run build && npm testwill build the code and run tests.npm run benchwill run benchmarksnpm run bundlewill build single non-module file
Speed
npm run bench
Benchmarks measured with Apple M4. noble-curves enable faster performance.
keygen x 7,267 ops/sec @ 137μs/op
sign x 6,888 ops/sec @ 145μs/op
verify x 788 ops/sec @ 1ms/op
getSharedSecret x 654 ops/sec @ 1ms/op
recoverPublicKey x 766 ops/sec @ 1ms/op
signAsync x 4,353 ops/sec @ 229μs/op verifyAsync x 773 ops/sec @ 1ms/op
Point.fromBytes x 13,322 ops/sec @ 75μs/op
License
The MIT License (MIT)
Copyright (c) 2019 Paul Miller (https://paulmillr.com)
See LICENSE file.