Skip to content

ECDH deriveBits: step 5's OperationError precedes the curve-mismatch check, but no browser does that #560

Description

@lahma

The prose

ECDH "Derive Bits" orders
its checks like this:

  1. Let publicKey be the public member of normalizedAlgorithm.
  2. If the [[type]] of publicKey is not "public"InvalidAccessError.
  3. If the name of publicKey's [[algorithm]]normalizedAlgorithm's nameInvalidAccessError.
  4. 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.
  5. If length is not null and is greater than maximumLengthOperationError.
  6. If the [[type]] of key is not "private"InvalidAccessError.
  7. If the name of publicKey's [[algorithm]] ≠ the name of key's [[algorithm]]InvalidAccessError.
  8. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions