---
title: "P-384"
description: "P-384 point math and ECDSA verification for ES384 and certificates"
canonical_url: "https://curves.agntn.dev/math/p384"
---
# P-384

> P-384 point math and ECDSA verification for ES384 and certificates

## P-384, also called secp384r1

P-384 is here for ES384, certificates and CNSA suites. SEC 2 v2 calls it secp384r1 in section 2.5.1. OpenSSL calls it secp384r1 too.

It uses y² = x³ - 3x + b over a 384-bit prime. The cofactor is 1. That's the same a = -3 shape as [P-256](https://curves.agntn.dev/math/p256).

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

multiplyGenerator(1n); // "03aa87ca22be8b05…3872760ab7"
isOnCurve(`02${"1".padStart(96, "0")}`); // false
```

Coordinates and scalars take 48 bytes, or 96 hex digits. P-256 takes 32 bytes. Compressed P-384 points take 49 bytes. Uncompressed points take 97.

## Verify a P-384 ECDSA signature

Have an ES384 token or a TLS certificate to check? Import `verify`, supply the digest and public key, and pass the signature.

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

const publicKey =
  "04ec3a4e415b4e19a4568618029f427fa5da9a8bc4ae92e02e06aae5286b300c64def8f0ea9055866064a254515480bc13" +
  "8015d9b72d7d57244ea8ef9ac0c621896708a59367f9dfb9f54ca84b3f1c9db1288b231c3ae0d4fe7344fd2533264720";
const digest =
  "9a9083505bc92276aec4be312696ef7bf3bf603f4bbd381196a029f340585312313bca4a9b5b890efee42c77b1ee25fe"; // SHA-384 of "sample"
const signature =
  "94edbb92a5ecb8aad4736e56c691916b3f88140666ce9fa73d64c4ea95ad133c81a648152e44acf96e36dd1e80fabe46" +
  "99ef4aeb15f178cea1fe40db2603138f130e740a19624526203b6351d0a3a94fa329c145786e679e7b82c71a38628ac8";

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.6. The RFC signed the six bytes of "sample" with SHA-384.

The signature is r followed by s. That's 96 bytes, or 192 hex digits. That's a lot to paste. The public key is compressed or uncompressed SEC1 hex.

### What about DER and the digest?

A JWS ES384 signature is already r then s. X.509 sends DER, so unpack r and s first.

Hash the signed bytes yourself. ES384 uses SHA-384. The digest takes 1 to 64 bytes as hex, without `0x`. Past 48 bytes, only the leftmost 384 bits count. SHA-512 works too. A SHA-256 digest is shorter than n and counts whole.

### What does it refuse?

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

Wrong shapes throw. A compressed P-256 point is 33 bytes, not 49. It's the wrong width for P-384.

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 p384 <operation>`. Verification takes the same inputs.

```bash
curves p384 verify --point 04ec3a4e415b4e19a4568618029f427fa5da9a8bc4ae92e02e06aae5286b300c64def8f0ea9055866064a254515480bc138015d9b72d7d57244ea8ef9ac0c621896708a59367f9dfb9f54ca84b3f1c9db1288b231c3ae0d4fe7344fd2533264720 --digest 9a9083505bc92276aec4be312696ef7bf3bf603f4bbd381196a029f340585312313bca4a9b5b890efee42c77b1ee25fe --signature 94edbb92a5ecb8aad4736e56c691916b3f88140666ce9fa73d64c4ea95ad133c81a648152e44acf96e36dd1e80fabe4699ef4aeb15f178cea1fe40db2603138f130e740a19624526203b6351d0a3a94fa329c145786e679e7b82c71a38628ac8
```

It prints:

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

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

## P-384 point math

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

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

These are the same export names as P-256. The `p384` export holds the domain parameters. Multiplication accepts scalars from 1 to n - 1. Pass a bigint or hex with at most 96 digits.

There's no signing, key recovery or scalar math. Need more room? [P-521](https://curves.agntn.dev/math/p521) takes 66 bytes a number. Same width, other curve? That's [brainpoolP384r1](https://curves.agntn.dev/math/brainpoolp384r1).

## Written from the specs

P-384 shares its SEC 1 code with P-256, [P-521](https://curves.agntn.dev/math/p521), [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). The shared code gets its widths from the curve. Seven curves, one implementation, no copied module. That's the right way to add a curve.

Hex lengths, the comb and the digest cut follow those widths. The math doesn't change, only the ruler does. Multiplication walks 96 digits on P-384. It walks 64 on the 32-byte curves. The P-384 verification table has 96 rows of 15 points. Messages swap the curve's name and widths.

Addition and doubling use algorithms 4 and 6 from Renes, Costello and Batina (2016). They're the same a = -3 formulas as P-256. The formulas are complete. Infinity and doubling need no branch. Square roots take one power because p is 3 mod 4.

Multiplication takes the same steps for every scalar. `test/p384.test.ts` times 1 against n - 1. JavaScript bigints promise nothing about time. Keep real money away.

Verification reads G from a faster table. Its scalars are public. In a local run, G multiplication took about 1.4 ms. Another point took about 3.5 ms. Verification took about 3.6 ms. `@noble/curves` 2.4.0 took about 5.5 ms on the same signatures.

The tests cover all 280 Wycheproof ECDSA P-384 SHA-384 vectors in P1363 form. That's r followed by s. They also check RFC 6979 appendix A.2.6 signatures. Those cover SHA-256, SHA-384 and SHA-512 for "sample" and "test".

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.


## Sitemap

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