grafana/k6 · error

failed to convert ECDSA key to ECDH key: %w

Error message

failed to convert ECDSA key to ECDH key: %w

What it means

When importing an ECDH JWK, k6 parses it as an ECDSA private key and then calls Go's (*ecdsa.PrivateKey).ECDH() to obtain the ECDH form. That conversion fails when the public point (x,y) is not on the named curve ('crypto/ecdsa: invalid public key') — the curve itself is already restricted to NIST P-256/P-384/P-521 by the earlier parsing. The error is wrapped with this message and returned from crypto.subtle.importKey('jwk', ...) with {name:'ECDH'}.

Source

Thrown at internal/js/modules/k6/webcrypto/jwk.go:279

	return &ecdsa.PrivateKey{
		PublicKey: *pk,
		D:         new(big.Int).SetBytes(d),
	}, PrivateCryptoKeyType, nil
}

func importECDHJWK(_ EllipticCurveKind, jsonKeyData []byte) (any, CryptoKeyType, error) {
	// first we do try to parse the key as ECDSA key
	key, _, err := importECDSAJWK(EllipticCurveKindP256, jsonKeyData)
	if err != nil {
		return nil, UnknownCryptoKeyType, fmt.Errorf("failed to parse input as ECDH key: %w", err)
	}

	switch key := key.(type) {
	case *ecdsa.PrivateKey:
		ecdhKey, err := key.ECDH()
		if err != nil {
			return nil, UnknownCryptoKeyType, fmt.Errorf("failed to convert ECDSA key to ECDH key: %w", err)
		}

		return ecdhKey, PrivateCryptoKeyType, nil
	case *ecdsa.PublicKey:
		ecdhKey, err := key.ECDH()
		if err != nil {
			return nil, UnknownCryptoKeyType, fmt.Errorf("failed to convert ECDSA key to ECDH key: %w", err)
		}

		return ecdhKey, PublicCryptoKeyType, nil
	default:
		return nil, UnknownCryptoKeyType, errors.New("input isn't a valid ECDH key")
	}
}

type rsaJWK struct {
	Kty string `json:"kty"`          // Key Type
	N   string `json:"n"`            // Modulus

View on GitHub (pinned to 93accf6570)

Solutions

  1. Verify the JWK comes from a real exported key pair (export it again from the source)
  2. Check that decoded x and y each have the byte length of the named curve (32/48/66 bytes) and belong to the same key
  3. Re-generate the ECDH key pair with crypto.subtle.generateKey and export a fresh JWK
  4. Use the ECDSA importer on the same JWK to cross-check: ECDSA import does not validate the point, so also verify by attempting a derive operation

Example fix

// before
const badJwk = { kty: 'EC', crv: 'P-256', x: 'AAAA...', y: 'AAAA...', d: 'AAAA...' }; // not on curve
const key = await crypto.subtle.importKey('jwk', badJwk, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
// after
const pair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits']);
const goodJwk = await crypto.subtle.exportKey('jwk', pair.privateKey);
const key = await crypto.subtle.importKey('jwk', goodJwk, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits']);
Defensive patterns

Strategy: try-catch

Validate before calling

const CURVE_BYTES = { 'P-256': 32, 'P-384': 48, 'P-521': 66 };
function b64uLen(s) { const n = s.length; return n - (n % 4 === 2 ? 1 : n % 4 === 3 ? 2 : 0); } // approx byte count
function coordLengthsPlausible(jwk) {
  const want = CURVE_BYTES[jwk.crv];
  return want && b64uLen(jwk.x) === want && b64uLen(jwk.y) === want;
}
if (!coordLengthsPlausible(jwk)) throw new Error('x/y lengths do not match crv; key likely corrupted');

Type guard

function hasCurveSizedCoords(jwk) {
  const want = { 'P-256': 43, 'P-384': 64, 'P-521': 88 }[jwk.crv];
  return !!want && jwk.x.length === want && jwk.y.length === want;
}

Try / catch

try {
  key = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDH', namedCurve: jwk.crv }, true, ['deriveBits']);
} catch (e) {
  if (e.message.includes('failed to convert ECDSA key to ECDH key')) {
    throw new Error('x/y are not a valid point on ' + jwk.crv + '; re-export the key from its origin');
  }
  throw e;
}

Prevention

When it happens

Trigger: A JWK whose x/y decode successfully as base64url but do not form a valid point on the curve named by crv: coordinates fabricated, corrupted, swapped, or taken from a different curve than crv claims; a d value paired with mismatched x/y.

Common situations: Hand-crafted test keys with arbitrary coordinate strings; JWKs corrupted in transit or by templating; copying x from one key and y from another; upstream key services emitting inconsistent keys.

Related errors


AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15). Data as JSON: /api/errors/13767c17e2cf05e5. Report an issue: GitHub.