P-384
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.
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.
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?
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.
curves p384 verify --point 04ec3a4e415b4e19a4568618029f427fa5da9a8bc4ae92e02e06aae5286b300c64def8f0ea9055866064a254515480bc138015d9b72d7d57244ea8ef9ac0c621896708a59367f9dfb9f54ca84b3f1c9db1288b231c3ae0d4fe7344fd2533264720 --digest 9a9083505bc92276aec4be312696ef7bf3bf603f4bbd381196a029f340585312313bca4a9b5b890efee42c77b1ee25fe --signature 94edbb92a5ecb8aad4736e56c691916b3f88140666ce9fa73d64c4ea95ad133c81a648152e44acf96e36dd1e80fabe4699ef4aeb15f178cea1fe40db2603138f130e740a19624526203b6351d0a3a94fa329c145786e679e7b82c71a38628ac8
It prints:
{"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,subtractPointsandnegatePointmultiplyPointandmultiplyGeneratorliftX,isOnCurveandconvertPointverify,p384andINVALID_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 takes 66 bytes a number. Same width, other curve? That's brainpoolP384r1.
Written from the specs
P-384 shares its SEC 1 code with P-256, P-521, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1 and 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.