---
title: "P-256"
description: "P-256 point math and ECDSA verification for passkeys and ES256"
canonical_url: "https://curves.agntn.dev/math/p256"
---
# P-256

> P-256 point math and ECDSA verification for passkeys and ES256

## secp256r1, not secp256k1

P-256 sits next to secp256k1. SEC 2 v2 calls it secp256r1 in section 2.4.2. OpenSSL calls it prime256v1. One curve, three names.

The k means Koblitz. The r means random, for verifiably random parameters. P-256 uses y² = x³ - 3x + b over a 256-bit prime. secp256k1 uses y² = x³ + 7. One letter apart, different curves.

```ts
import { isOnCurve, multiplyGenerator } from "@agntn/curves/p256";

multiplyGenerator(1n); // "036b17d1f2e12c42…d898c296"
isOnCurve("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); // false
```

Both curves use SEC1 hex for points. Compressed points take 33 bytes. Uncompressed points take 65. secp256k1's G looks like a key, but it's knocking on the wrong door.

## Verify a P-256 ECDSA signature

P-256 turns up in WebAuthn passkeys and ES256 tokens. It's also in TLS certificates and OpenPGP keys on NIST curves.

```ts
import { verify } from "@agntn/curves/p256";

const publicKey =
  "0460fed4ba255a9d31c961eb74c6356d68c049b8923b61fa6ce669622e60f29fb6" +
  "7903fe1008b8bc99a41ae9e95628bc64f2f1b20c2d7e9f5177a3c294d4462299";
const digest = "af2bdbe1aa9b6ec1e2ade1d694f41fc71a831d0268e9891562113d8a62add1bf"; // SHA-256 of "sample"
const signature =
  "efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716" +
  "f7cb1c942d657c41d436c7a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8";

verify(signature, { digest, publicKey }); // true
verify(signature, { digest: `00${digest.slice(2)}`, publicKey }); // false
```

The key and signature come from RFC 6979, appendix A.2.5. The RFC signed the six bytes of "sample". Deterministically, with SHA-256.

The signature is r followed by s. It takes 128 hex digits. The digest is the hash of the signed bytes. The public key is compressed or uncompressed SEC1 hex. Keys and signatures never take a `0x` prefix.

### What about DER and the digest?

Got a passkey assertion or a certificate? Their signatures are DER, so unpack r and s before calling `verify`. A JWS ES256 signature is already r then s, 64 bytes.

Hash the signed bytes yourself. ES256 and WebAuthn use SHA-256. The digest takes 1 to 64 bytes as hex, without `0x`. Past 32 bytes, SEC 1 counts only the leftmost 256 bits.

### What does it refuse?

```text
verify("00", { digest, publicKey })                        // Signature must be 128 hex digits without 0x, r then s
verify(signature, { digest: "0x00", publicKey })           // Digest must be hex without 0x, 1 to 64 bytes
verify(signature, { digest, publicKey: secp256k1G })       // Invalid SEC1 P-256 public key
liftX("0000000000000000000000000000000000000000000000000000000000000001")  // No point of P-256 has this x
```

Wrong shapes throw. An r or s outside 1 to n - 1 returns `false`. The optional `lowS` is `false` by default. Set it to `true` to refuse s above n / 2.

### CLI and agent tool

The CLI uses `curves p256 <operation>`. Verification takes the same inputs.

```bash
curves p256 verify --point 0460fed4ba255a9d31c961eb74c6356d68c049b8923b61fa6ce669622e60f29fb67903fe1008b8bc99a41ae9e95628bc64f2f1b20c2d7e9f5177a3c294d4462299 --digest af2bdbe1aa9b6ec1e2ade1d694f41fc71a831d0268e9891562113d8a62add1bf --signature efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716f7cb1c942d657c41d436c7a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8
```

It prints:

```text
{"operation":"verify","valid":true}
```

The CLI offers `add`, `subtract`, `negate`, `multiply`, `lift`, `check` and `verify`. The agent tool `curves_p256_compute` offers the same operations. Its arguments are `point`, `other`, `scalar`, `x`, `digest`, `signature`, `lowS` and `compressed`.

## P-256 point math

The `@agntn/curves/p256` entry exports `addPoints`, `subtractPoints`, `negatePoint`, `multiplyPoint`, `multiplyGenerator`, `liftX`, `isOnCurve`, `convertPoint` and `verify`. It also exports `p256` and `INVALID_POINT`.

There's no signing, key recovery or scalar math here. Need a wider curve? [P-384](https://curves.agntn.dev/math/p384) and [P-521](https://curves.agntn.dev/math/p521) are here too. Same length, other curve? That's [brainpoolP256r1](https://curves.agntn.dev/math/brainpoolp256r1).

## Written from the specs

Both curves run on one SEC 1 code. Shared functions behave the same, and the messages only swap the curve's name.

Only addition and doubling differ. For a = -3, P-256 takes algorithms 4 and 6 from Renes, Costello and Batina (2016). secp256k1 takes 7 and 9 for a = 0. The formulas are complete. Infinity and doubling need no branch.

Multiplication takes the same steps for every scalar. In a local run, G took about 0.5 ms. Any other point took about 1.3 ms. A fixed scalar and random ones stayed within 0.33 percent in a local timing probe. Is that constant time? The steps are. JavaScript's bigints promise nothing, so keep real money away from it.

Verification reads G from a faster table. Its scalars are public. In a local run, verification took about 1.5 ms. `@noble/curves` 2.4.0 took about 2.2 ms on the same signatures.

The tests cover all 262 Wycheproof ECDSA P-256 SHA-256 vectors in r then s form. They check RFC 6979 A.2.5 signatures over SHA-256 and SHA-512. Multiples of G are frozen from `@noble/curves` 2.4.0. The root entry's generic curve code gets edge scalars too. A local run against noble agreed on 300 random cases.


## Sitemap

See the full [sitemap](https://curves.agntn.dev/sitemap.md) for all pages.
