The prose
ECDH "Derive Bits" orders
its checks like this:
- Let publicKey be the
public member of normalizedAlgorithm.
- If the
[[type]] of publicKey is not "public" → InvalidAccessError.
- If the
name of publicKey's [[algorithm]] ≠ normalizedAlgorithm's name → InvalidAccessError.
- Let maximumLength be the length in bits of the output of the field element to octet
string conversion … for the EC domain parameters associated with publicKey.
- If length is not null and is greater than maximumLength → OperationError.
- If the
[[type]] of key is not "private" → InvalidAccessError.
- If the
name of publicKey's [[algorithm]] ≠ the name of key's [[algorithm]] → InvalidAccessError.
- If the
namedCurve of publicKey's [[algorithm]] ≠ the namedCurve of key's [[algorithm]] → InvalidAccessError.
9.–11. the ECDH primitive, and the truncation of secret.
Steps 4 and 5 measure the ceiling off the public key alone and raise before steps 7 and
8 have established that the two keys are a pair at all. So for a mismatched pair where the
public key is on the narrower curve, the request is refused for its length rather than
for the mismatch — a P-521 base key, a P-256 public key and length: 528 is an
OperationError under a literal reading, not an InvalidAccessError.
The browsers
All of them answer InvalidAccessError. Chromium
(components/webcrypto/algorithms/ecdh.cc), Gecko (dom/crypto/WebCryptoTask.cpp), WebKit
(CryptoAlgorithmECDH::deriveBits) and Node (lib/internal/crypto/diffiehellman.js) all
validate the key roles and the curve equality first and only look at length once secret
exists — several of them have no maximumLength step at all and rely on step 11's
"if the length in bits of secret is less than length".
The WPT rows that pin it
WebCryptoAPI/derive_bits_keys/ecdh_bits.https.any.js builds its mismatched-curve cases in
derive_bits_keys/ecdh.js, pairing each curve with P-256 (or, for P-256 itself, with
P-384), and derive.js's derive() defaults length to 8 * size where size is the
base key's field width. So:
| row |
base key |
public key |
length |
step 5 says |
asserted |
P-384 mismatched curves |
P-384 |
P-256 |
384 |
OperationError (384 > 256) |
InvalidAccessError |
P-521 mismatched curves |
P-521 |
P-256 |
528 |
OperationError (528 > 256) |
InvalidAccessError |
P-256 mismatched curves |
P-256 |
P-384 |
256 |
passes (256 ≤ 384) |
InvalidAccessError |
The first two are unsatisfiable by an implementation that runs step 5 where it is written.
The third is green either way, which is why this is easy to miss. ecdh_bits.https.any.html
is 40/40 in Chrome 152, Firefox 154 and Safari 26.6 on wpt.fyi (master/stable,
2026-08-21), so the tests are not aspirational — the spec is simply describing a different
algorithm from the one that shipped.
Suggested fix
Move steps 4 and 5 to after step 8, so the order becomes 1, 2, 3, 6, 7, 8, then the
domain-parameter/length steps, then the primitive. Nothing else needs to change:
- Every one of steps 2, 3, 6, 7 and 8 is a comparison of already-validated key metadata, so
moving the ceiling past them costs nothing and cannot make a previously-succeeding call fail.
- Once step 8 has run, "the EC domain parameters associated with publicKey" and those
associated with key are the same parameters, so the wording of step 4 need not change at
all — only its position. (If you would rather make that explicit, step 4 could equally read
"associated with key" after the move.)
- The only observable difference is exactly the one the WPT rows assert: a mismatched pair
now reports the mismatch whatever length asked for, instead of reporting a length that
was never going to be derivable.
A smaller alternative — leaving the steps where they are but deriving maximumLength from
key rather than publicKey — would make the two rows above pass, but it leaves an
OperationError reachable for a pair that step 8 is about to reject anyway (e.g. a P-256
base key, a P-384 public key and length: 264), which is still not what browsers do.
Reordering is the change that matches shipped behaviour in every case.
Found while running the WebCryptoAPI corpus against
Jint's Web Crypto implementation
(sebastienros/jint#3180); Jint has taken the browsers' order and documented the divergence
rather than keep two red WPT rows.
The prose
ECDH "Derive Bits" orders
its checks like this:
publicmember of normalizedAlgorithm.[[type]]of publicKey is not"public"→ InvalidAccessError.nameof publicKey's[[algorithm]]≠ normalizedAlgorithm'sname→ InvalidAccessError.string conversion … for the EC domain parameters associated with publicKey.
[[type]]of key is not"private"→ InvalidAccessError.nameof publicKey's[[algorithm]]≠ thenameof key's[[algorithm]]→ InvalidAccessError.namedCurveof publicKey's[[algorithm]]≠ thenamedCurveof key's[[algorithm]]→ InvalidAccessError.9.–11. the ECDH primitive, and the truncation of secret.
Steps 4 and 5 measure the ceiling off the public key alone and raise before steps 7 and
8 have established that the two keys are a pair at all. So for a mismatched pair where the
public key is on the narrower curve, the request is refused for its length rather than
for the mismatch — a P-521 base key, a P-256 public key and
length: 528is anOperationErrorunder a literal reading, not anInvalidAccessError.The browsers
All of them answer
InvalidAccessError. Chromium(
components/webcrypto/algorithms/ecdh.cc), Gecko (dom/crypto/WebCryptoTask.cpp), WebKit(
CryptoAlgorithmECDH::deriveBits) and Node (lib/internal/crypto/diffiehellman.js) allvalidate the key roles and the curve equality first and only look at length once secret
exists — several of them have no maximumLength step at all and rely on step 11's
"if the length in bits of secret is less than length".
The WPT rows that pin it
WebCryptoAPI/derive_bits_keys/ecdh_bits.https.any.jsbuilds its mismatched-curve cases inderive_bits_keys/ecdh.js, pairing each curve withP-256(or, forP-256itself, withP-384), andderive.js'sderive()defaults length to8 * sizewhere size is thebase key's field width. So:
P-384 mismatched curvesP-521 mismatched curvesP-256 mismatched curvesThe first two are unsatisfiable by an implementation that runs step 5 where it is written.
The third is green either way, which is why this is easy to miss.
ecdh_bits.https.any.htmlis 40/40 in Chrome 152, Firefox 154 and Safari 26.6 on wpt.fyi (master/stable,
2026-08-21), so the tests are not aspirational — the spec is simply describing a different
algorithm from the one that shipped.
Suggested fix
Move steps 4 and 5 to after step 8, so the order becomes 1, 2, 3, 6, 7, 8, then the
domain-parameter/length steps, then the primitive. Nothing else needs to change:
moving the ceiling past them costs nothing and cannot make a previously-succeeding call fail.
associated with key are the same parameters, so the wording of step 4 need not change at
all — only its position. (If you would rather make that explicit, step 4 could equally read
"associated with key" after the move.)
now reports the mismatch whatever length asked for, instead of reporting a length that
was never going to be derivable.
A smaller alternative — leaving the steps where they are but deriving maximumLength from
key rather than publicKey — would make the two rows above pass, but it leaves an
OperationErrorreachable for a pair that step 8 is about to reject anyway (e.g. a P-256base key, a P-384 public key and
length: 264), which is still not what browsers do.Reordering is the change that matches shipped behaviour in every case.
Found while running the WebCryptoAPI corpus against
Jint's Web Crypto implementation
(sebastienros/jint#3180); Jint has taken the browsers' order and documented the divergence
rather than keep two red WPT rows.