{"record":{"id":"13767c17e2cf05e5","repo":"grafana/k6","slug":"failed-to-convert-ecdsa-key-to-ecdh-key-w","errorCode":null,"errorMessage":"failed to convert ECDSA key to ECDH key: %w","messagePattern":"failed to convert ECDSA key to ECDH key: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/js/modules/k6/webcrypto/jwk.go","lineNumber":279,"sourceCode":"\n\treturn &ecdsa.PrivateKey{\n\t\tPublicKey: *pk,\n\t\tD:         new(big.Int).SetBytes(d),\n\t}, PrivateCryptoKeyType, nil\n}\n\nfunc importECDHJWK(_ EllipticCurveKind, jsonKeyData []byte) (any, CryptoKeyType, error) {\n\t// first we do try to parse the key as ECDSA key\n\tkey, _, err := importECDSAJWK(EllipticCurveKindP256, jsonKeyData)\n\tif err != nil {\n\t\treturn nil, UnknownCryptoKeyType, fmt.Errorf(\"failed to parse input as ECDH key: %w\", err)\n\t}\n\n\tswitch key := key.(type) {\n\tcase *ecdsa.PrivateKey:\n\t\tecdhKey, err := key.ECDH()\n\t\tif err != nil {\n\t\t\treturn nil, UnknownCryptoKeyType, fmt.Errorf(\"failed to convert ECDSA key to ECDH key: %w\", err)\n\t\t}\n\n\t\treturn ecdhKey, PrivateCryptoKeyType, nil\n\tcase *ecdsa.PublicKey:\n\t\tecdhKey, err := key.ECDH()\n\t\tif err != nil {\n\t\t\treturn nil, UnknownCryptoKeyType, fmt.Errorf(\"failed to convert ECDSA key to ECDH key: %w\", err)\n\t\t}\n\n\t\treturn ecdhKey, PublicCryptoKeyType, nil\n\tdefault:\n\t\treturn nil, UnknownCryptoKeyType, errors.New(\"input isn't a valid ECDH key\")\n\t}\n}\n\ntype rsaJWK struct {\n\tKty string `json:\"kty\"`          // Key Type\n\tN   string `json:\"n\"`            // Modulus","sourceCodeStart":261,"sourceCodeEnd":297,"githubUrl":"https://github.com/grafana/k6/blob/93accf6570dcd306ca5e99cc44c393ee3797761b/internal/js/modules/k6/webcrypto/jwk.go#L261-L297","documentation":"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'}.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify the JWK comes from a real exported key pair (export it again from the source)","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","Re-generate the ECDH key pair with crypto.subtle.generateKey and export a fresh JWK","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"],"exampleFix":"// before\nconst badJwk = { kty: 'EC', crv: 'P-256', x: 'AAAA...', y: 'AAAA...', d: 'AAAA...' }; // not on curve\nconst key = await crypto.subtle.importKey('jwk', badJwk, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);\n// after\nconst pair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits']);\nconst goodJwk = await crypto.subtle.exportKey('jwk', pair.privateKey);\nconst key = await crypto.subtle.importKey('jwk', goodJwk, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits']);","handlingStrategy":"try-catch","validationCode":"const CURVE_BYTES = { 'P-256': 32, 'P-384': 48, 'P-521': 66 };\nfunction b64uLen(s) { const n = s.length; return n - (n % 4 === 2 ? 1 : n % 4 === 3 ? 2 : 0); } // approx byte count\nfunction coordLengthsPlausible(jwk) {\n  const want = CURVE_BYTES[jwk.crv];\n  return want && b64uLen(jwk.x) === want && b64uLen(jwk.y) === want;\n}\nif (!coordLengthsPlausible(jwk)) throw new Error('x/y lengths do not match crv; key likely corrupted');","typeGuard":"function hasCurveSizedCoords(jwk) {\n  const want = { 'P-256': 43, 'P-384': 64, 'P-521': 88 }[jwk.crv];\n  return !!want && jwk.x.length === want && jwk.y.length === want;\n}","tryCatchPattern":"try {\n  key = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDH', namedCurve: jwk.crv }, true, ['deriveBits']);\n} catch (e) {\n  if (e.message.includes('failed to convert ECDSA key to ECDH key')) {\n    throw new Error('x/y are not a valid point on ' + jwk.crv + '; re-export the key from its origin');\n  }\n  throw e;\n}","preventionTips":["Never hand-craft x/y values; always export real keys","Check that x and y come from the same key and the same curve as crv","Coordinate length checks catch most corruption before import"],"tags":["webcrypto","jwk","import","ecdh","elliptic-curve","k6"],"backgroundTag":null,"analyzedSha":"93accf6570dcd306ca5e99cc44c393ee3797761b","analyzedAt":"2026-08-15T21:23:27.118Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}