P-256 vs secp256k1 is rarely a free choice: the curve usually comes with the ecosystem. Both are 256-bit prime-order Weierstrass curves with about 128-bit security and identical key and signature sizes, but they differ in origin, in which platforms support them and in the conventions layered on top, and moving data between the two worlds is where bugs happen.
At a glance
| P-256 (secp256r1, prime256v1) | secp256k1 | |
|---|---|---|
| Specified in | NIST SP 800-186 §3.2.1.3, SEC 2 §2.4.2 | SEC 2 §2.4.1; allowed by SP 800-186 App. H.2 for blockchain use |
| Equation | y² = x³ − 3x + b over p = 2²⁵⁶ − 2²²⁴ + 2¹⁹² + 2⁹⁶ − 1 | y² = x³ + 7 over p = 2²⁵⁶ − 2³² − 977 |
| Parameter origin | b derived from an unexplained SHA-1 seed | Fixed, minimal coefficients (Koblitz-style) |
| Cofactor, security | 1, about 128 bits | 1, about 128 bits |
| Keys and signatures | 32-byte private key, 33/65-byte public key, 64-byte raw signature | Same |
| OID | 1.2.840.10045.3.1.7 | 1.3.132.0.10 |
| JOSE / COSE | ES256 / −7 | ES256K / −47 (RFC 8812) |
| Node.js 26 (OpenSSL 3.6.5), sign / verify per second | about 68,800 / 27,200 | about 6,500 / 7,200 |
| @noble/curves 2.3.0, sign / verify per second | about 3,800 / 870 | about 3,800 / 1,180 |
| WebCrypto, TLS 1.3, WebAuthn | Yes | No |
| Apple Secure Enclave, PIV cards, most HSMs | Yes | Rarely (AWS KMS offers ECC_SECG_P256K1) |
| Typical users | TLS certificates, passkeys, JWTs, device identity | Bitcoin, Ethereum, Nostr, most EVM chains |
Measured on one core of an Apple M4 Max.
Origins and trust
P-256 was published by NIST in 1999 and standardized in FIPS 186-2 (2000), and by SECG as secp256r1. Its prime has a special shape that enables fast reduction, and its coefficient b was generated from a seed through SHA-1, as the "verifiably random" procedure of the time prescribed. The seed's origin was never explained, which after the Dual_EC_DRBG revelations led some designers to distrust NIST curves. No attack on P-256 based on that seed has ever been found, and it remains the most scrutinized curve in deployment.
secp256k1 comes from SEC 2 (2000). With a = 0 and b = 7 there is nothing to hide, and its special structure provides an efficiently computable endomorphism (the GLV method) that speeds up scalar multiplication. Satoshi Nakamoto chose it for Bitcoin in 2009, an unusual choice at the time, and Ethereum followed. NIST SP 800-186 (2023) explicitly allows secp256k1 "for blockchain-related applications".
Both curves have prime order, so there are no cofactor subtleties, and both fall equally to Shor's algorithm.
Platform and library support
This is the real difference. P-256 is built into everything that manages keys for you: WebCrypto in every browser, the TLS stacks of every operating system, Apple's Secure Enclave and CryptoKit, Android Keystore, TPM 2.0, PIV smart cards, and every cloud KMS. Passkeys use it by default, and the CA/Browser Forum Baseline Requirements allow it for publicly trusted certificates.
secp256k1 is absent from WebCrypto and from TLS 1.3 (RFC 8422 had already deprecated its TLS 1.2 codepoint), and general-purpose libraries treat it as an afterthought. Blockchain software therefore relies on dedicated implementations: Bitcoin Core's constant-time libsecp256k1, and @noble/curves, Rust's k256 and Go's go-ethereum/crypto elsewhere. Hardware wallets support it; enterprise HSMs vary, and AWS KMS offers it as the ECC_SECG_P256K1 key spec for signing only.
Performance
With equal sizes, speed depends on implementation quality rather than the curve. OpenSSL has hand-tuned assembly for P-256 and generic code for secp256k1, so Node.js 26.10 (OpenSSL 3.6.5) signs about ten times faster with P-256: about 68,800 against 6,500 signatures per second on one Apple M4 Max core. In pure JavaScript the picture flips slightly: @noble/curves 2.3.0 signs at the same rate on both curves and verifies secp256k1 about a third faster, thanks to the endomorphism. libsecp256k1 is faster still. The lesson: benchmark the library you will ship, not the curve.
Conventions that differ
The same ECDSA math travels with different rules in each world.
- Hashing. P-256 pairs with SHA-256 (
ES256,ecdsa-with-SHA256). Bitcoin signs double SHA-256 digests; Ethereum signs Keccak-256 digests, which are not SHA3-256. - Encoding. X.509 and TLS use DER; JOSE and WebCrypto use raw
r‖s; Ethereum appends a recovery bytevfor 65 bytes total, and Bitcoin's signed messages put a header byte in front. - Public key recovery. Ethereum never sends public keys: recovery (
ecrecover) computes the key from the signature and the address is compared. P-256 protocols transmit or certify the key instead. - Low-S. Every ECDSA signature
(r, s)has a twin(r, n − s). Bitcoin relay policy and Ethereum transaction rules (EIP-2) accept only the low-S form; generic P-256 verifiers accept both. - Schnorr. Bitcoin's Taproot added 64-byte BIP-340 Schnorr signatures with 32-byte x-only keys on secp256k1, also used by Nostr. There is no standard Schnorr scheme on P-256.
The malleability is easy to demonstrate with Node.js and its OpenSSL-backed secp256k1 support:
import { generateKeyPairSync, sign, verify } from "node:crypto";
// Group order of secp256k1 (SEC 2 §2.4.1).
const n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "secp256k1" });
const message = Buffer.from("hello");
const raw = sign("sha256", message, { key: privateKey, dsaEncoding: "ieee-p1363" });
const r = raw.subarray(0, 32);
const s = BigInt("0x" + raw.subarray(32).toString("hex"));
const flipped = Buffer.concat([r, Buffer.from((n - s).toString(16).padStart(64, "0"), "hex")]);
const check = (sig) => verify("sha256", message, { key: publicKey, dsaEncoding: "ieee-p1363" }, sig);
console.log("(r, s) and (r, n - s) both verify:", check(raw) && check(flipped));
console.log("exactly one of them is low-S:", (s <= n / 2n) !== (n - s <= n / 2n));
(r, s) and (r, n - s) both verify: true
exactly one of them is low-S: true
OpenSSL neither produces nor enforces low-S, so signatures made this way must be normalized before Bitcoin or Ethereum will accept them. The Ethereum signature verifier flags high-S values.
When to choose which
- Web, TLS, passkeys, JWTs, mobile and device keys: P-256, because the platforms offer nothing else in hardware.
- Bitcoin, Ethereum and EVM chains, Nostr: secp256k1, with the chain's hashing, encoding, recovery and low-S rules.
- Bridging the two (for example a smart-contract wallet controlled by a passkey): keep each curve in its own lane. Ethereum's Fusaka upgrade (December 2025) added EIP-7951, a P-256 verification precompile at address
0x100costing 6,900 gas, and many rollups offer the compatible RIP-7212 precompile, so contracts can verify passkey signatures directly. - A new protocol with free choice: neither; consider Ed25519 for software keys or P-256 for hardware-backed keys.
Migration notes
Keys cannot be converted between the curves; changing curves means new keys and, in blockchains, new addresses.
- Do not reuse key material across curves. A 32-byte secret is a valid private key on both curves, but the resulting public keys are unrelated; treat that as a footgun, not a feature.
- When moving secp256k1 signing into an HSM or KMS, confirm the key type exists, expect DER output, convert to raw
r‖s, normalize to low-S and compute the recovery id yourself by trying both parities. - When verifying P-256 signatures in an EVM contract, use a verified library or a precompile where the chain offers one; never hand-roll modular arithmetic in Solidity.
- In JOSE, switching from
ES256KtoES256changesalg,crvand the key; publish both keys with distinctkidvalues during the transition.
References
- NIST SP 800-186, elliptic curve domain parameters (§3.2.1.3 and Appendix H.2)
- SEC 2 v2, recommended elliptic curve domain parameters (§2.4.1, §2.4.2)
- FIPS 186-5, Digital Signature Standard
- RFC 8812, ES256K and secp256k1 in JOSE and COSE
- BIP-340, Schnorr signatures for secp256k1
- BIP-146, LOW_S and EIP-2
- AWS KMS key spec reference