P-521
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 and P-256, it uses a = -3.
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.
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?
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.
curves p521 verify --point 0401894550d0785932e00eaa23b694f213f8c3121f86dc97a04e5a7167db4e5bcd371123d46e45db6b5d5370a7f20fb633155d38ffa16d2bd761dcac474b9a2f5023a400493101c962cd4d2fddf782285e64584139c2f91b47f87ff82354d6630f746a28a0db25741b5b34a828008b22acc23f924faafbd4d33f81ea66956dfeaa2bfdfcf5 --digest 39a5e04aaff7455d9850c605364f514c11324ce64016960d23d5dc57d3ffd8f49a739468ab8049bf18eef820cdb1ad6c9015f838556bc7fad4138b23fdf986c7 --signature 00c328fafcbd79dd77850370c46325d987cb525569fb63c5d3bc53950e6d4c5f174e25a1ee9017b5d450606add152b534931d7d4e8455cc91f9b15bf05ec36e377fa00617cce7cf5064806c467f678d3b4080d6f1cc50af26ca209417308281b68af282623eaa63e5b5c0723d8b8c37ff0777b1a20f8ccb1dccc43997f1ee0e44da4a67a
It prints:
{"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,subtractPointsandnegatePointmultiplyPointandmultiplyGeneratorliftX,isOnCurveandconvertPointverify,p521andINVALID_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, brainpoolP384r1, brainpoolP512r1 and 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.