---
title: "P-521"
description: "P-521 point math and ECDSA verification for ES512 and certificates"
canonical_url: "https://curves.agntn.dev/math/p521"
---
# P-521

> P-521 point math and ECDSA verification for ES512 and certificates

## P-521, also called secp521r1

P-521 handles points and signatures for ES512, OpenPGP keys and TLS certificates. SEC 2 v2 calls it secp521r1 in section 2.6.1. OpenSSL uses that name too.

The curve is y² = x³ - 3x + b over p = 2^521 - 1. That's a Mersenne prime. The cofactor is 1. Like [P-384](https://curves.agntn.dev/math/p384) and [P-256](https://curves.agntn.dev/math/p256), it uses a = -3.

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

multiplyGenerator(1n); // "0200c6858e06b704…7e31c2e5bd66"
isOnCurve(`02${"3".padStart(132, "0")}`); // false
```

Coordinates and scalars take 66 bytes, or 132 hex digits. That's 528 bits for a 521-bit number. The top byte is `00` or `01`. Its other seven bits stay zero. Compressed points take 67 bytes. Uncompressed points take 133.

## Verify a P-521 ECDSA signature

Checking an ES512 token or a certificate? Hash the signed bytes, then give `verify` the digest, signature and public key.

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

const publicKey =
  "0401894550d0785932e00eaa23b694f213f8c3121f86dc97a04e5a7167db4e5bcd371123d46e45db6b5d5370a7f20fb633" +
  "155d38ffa16d2bd761dcac474b9a2f5023a400493101c962cd4d2fddf782285e64584139c2f91b47f87ff82354d6630f74" +
  "6a28a0db25741b5b34a828008b22acc23f924faafbd4d33f81ea66956dfeaa2bfdfcf5";
const digest =
  "39a5e04aaff7455d9850c605364f514c11324ce64016960d23d5dc57d3ffd8f4" +
  "9a739468ab8049bf18eef820cdb1ad6c9015f838556bc7fad4138b23fdf986c7"; // SHA-512 of "sample"
const signature =
  "00c328fafcbd79dd77850370c46325d987cb525569fb63c5d3bc53950e6d4c5f174e25a1ee9017b5d450606add152b5349" +
  "31d7d4e8455cc91f9b15bf05ec36e377fa00617cce7cf5064806c467f678d3b4080d6f1cc50af26ca209417308281b68af" +
  "282623eaa63e5b5c0723d8b8c37ff0777b1a20f8ccb1dccc43997f1ee0e44da4a67a";

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

The key and signature are from RFC 6979 appendix A.2.7. The digest is SHA-512 over "sample". The second call replaces the digest's first byte. It doesn't just add a leading zero.

A signature holds r followed by s. That's 132 bytes, or 264 hex digits. Better get comfortable pasting. The public key is compressed or uncompressed SEC1 hex.

### What about DER and the digest?

A JWS ES512 signature already has r followed by s. X.509 uses DER and OpenPGP uses two MPIs. Unpack r and s first. Pad each to 132 hex digits.

The digest is 1 to 64 bytes of hex without `0x`. P-521's n has 521 bits. SHA-512 has 512, so the whole digest counts. For once, nothing gets cut. A shorter digest like SHA-256 counts whole too. Adding a leading `00` byte doesn't change its value.

### What does it refuse?

```text
verify("00", { digest, publicKey })                   // Signature must be 264 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: p384G })       // Invalid SEC1 P-521 public key
liftX("3".padStart(132, "0"))                         // No point of P-521 has this x
```

Wrong shapes throw. A compressed P-384 point is 49 bytes. P-521 needs 67.

An r or s outside 1 to n - 1 returns `false`. The `lowS` option defaults to `false`. Set it to `true` to refuse s above n / 2.

### CLI and agent tool

The CLI uses `curves p521 <operation>`. Here's the same signature through `verify`.

```bash
curves p521 verify --point 0401894550d0785932e00eaa23b694f213f8c3121f86dc97a04e5a7167db4e5bcd371123d46e45db6b5d5370a7f20fb633155d38ffa16d2bd761dcac474b9a2f5023a400493101c962cd4d2fddf782285e64584139c2f91b47f87ff82354d6630f746a28a0db25741b5b34a828008b22acc23f924faafbd4d33f81ea66956dfeaa2bfdfcf5 --digest 39a5e04aaff7455d9850c605364f514c11324ce64016960d23d5dc57d3ffd8f49a739468ab8049bf18eef820cdb1ad6c9015f838556bc7fad4138b23fdf986c7 --signature 00c328fafcbd79dd77850370c46325d987cb525569fb63c5d3bc53950e6d4c5f174e25a1ee9017b5d450606add152b534931d7d4e8455cc91f9b15bf05ec36e377fa00617cce7cf5064806c467f678d3b4080d6f1cc50af26ca209417308281b68af282623eaa63e5b5c0723d8b8c37ff0777b1a20f8ccb1dccc43997f1ee0e44da4a67a
```

It prints:

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

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

## P-521 point math

The `@agntn/curves/p521` entry exports:

- `addPoints`, `subtractPoints` and `negatePoint`
- `multiplyPoint` and `multiplyGenerator`
- `liftX`, `isOnCurve` and `convertPoint`
- `verify`, `p521` and `INVALID_POINT`

Those names match P-256 and P-384. The `p521` export holds the domain parameters. Give `liftX` an x as 132 hex digits.

Try x = 0, 1 or 2. Each has points on P-521. The first empty x is 3. On P-256 and P-384, x = 1 is already empty.

There's no signing, key recovery or scalar math on P-521. Like P-256 and P-384, it verifies signatures.

## Written from the specs

P-521 shares `src/core/sec1.ts` with P-256, P-384, [brainpoolP256r1](https://curves.agntn.dev/math/brainpoolp256r1), [brainpoolP384r1](https://curves.agntn.dev/math/brainpoolp384r1), [brainpoolP512r1](https://curves.agntn.dev/math/brainpoolp512r1) and [secp256k1](https://curves.agntn.dev/math/secp256k1). Seven curves, one implementation. Widths come from the curve. That's better than keeping seven copies in sync.

The shared code wants n and p of the same bit length. Here, both have 521 bits. Digest cuts follow those 521 bits, not the 528 bits in 66 bytes.

Multiplication walks 132 hex digits on P-521. P-384 walks 96, and the 32-byte curves walk 64. The comb for secret multiplies has four rows, 33 steps apart. The verification table has 132 rows of 15 points.

Addition and doubling use algorithms 4 and 6 from Renes, Costello and Batina (2016). They're the same a = -3 formulas used by P-256 and P-384. Square roots take one power because p is 3 mod 4.

A secret multiply takes the same steps for every scalar. `test/p521.test.ts` times 1 against n - 1. But JavaScript bigints promise nothing about time. The library is unaudited. Keep real money away.

In a local run, G multiplication took about 2.4 ms. Another point took about 5.5 ms. Verification took about 5.6 ms. `@noble/curves` 2.4.0 took about 7.4 ms on the same 50 signatures. In another local run, the generic code for curves you define took about 49 ms for one P-521 multiplication.

All 318 Wycheproof ECDSA P-521 SHA-512 vectors pass in P1363 form. That's r followed by s. The tests also check RFC 6979 appendix A.2.7 signatures for SHA-256, SHA-384 and SHA-512 over "sample" and "test". Those are checks worth having.

Multiples of G are frozen from `@noble/curves` 2.4.0. OpenSSL agreed on every one when they were frozen. In a local run, noble agreed on 300 random cases and 50 signatures. OpenSSL agreed on 200 random keys.


## Sitemap

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