diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 27f2a9f081..8fd5040639 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,20 +66,14 @@ jobs: test: needs: - build - continue-on-error: ${{ matrix.experimental || false }} strategy: fail-fast: false matrix: node-version: - - 12.20.0 - 12 - - 14.15.0 - 14 - - 16.13.0 - 16 - include: - - experimental: true - node-version: '>=17' + - 18 runs-on: ubuntu-latest steps: @@ -116,7 +110,7 @@ jobs: if: ${{ !startsWith(matrix.node-version, '14') && !startsWith(matrix.node-version, '12') }} - name: Test with Node.js Web API run: npm run test-webapi - if: ${{ matrix.experimental }} + if: ${{ !startsWith(matrix.node-version, '14') && !startsWith(matrix.node-version, '12') }} test-deno: needs: diff --git a/src/lib/crypto_key.ts b/src/lib/crypto_key.ts index 392719f056..3ecdf7cf69 100644 --- a/src/lib/crypto_key.ts +++ b/src/lib/crypto_key.ts @@ -75,6 +75,12 @@ export function checkSigCryptoKey(key: CryptoKey, alg: string, ...usages: KeyUsa if (!isAlgorithm(key.algorithm, 'NODE-ED25519')) throw unusable('NODE-ED25519') break } + case 'EdDSA': { + if (key.algorithm.name !== 'Ed25519' && key.algorithm.name !== 'Ed448') { + throw unusable('Ed25519 or Ed448') + } + break + } case 'ES256': case 'ES384': case 'ES512': { @@ -111,9 +117,17 @@ export function checkEncCryptoKey(key: CryptoKey, alg: string, ...usages: KeyUsa if (actual !== expected) throw unusable(expected, 'algorithm.length') break } - case 'ECDH': - if (!isAlgorithm(key.algorithm, 'ECDH')) throw unusable('ECDH') + case 'ECDH': { + switch (key.algorithm.name) { + case 'ECDH': + case 'X25519': + case 'X448': + break + default: + throw unusable('ECDH, X25519, or X448') + } break + } case 'PBES2-HS256+A128KW': case 'PBES2-HS384+A192KW': case 'PBES2-HS512+A256KW': diff --git a/src/runtime/browser/asn1.ts b/src/runtime/browser/asn1.ts index 4222166380..a9344253d9 100644 --- a/src/runtime/browser/asn1.ts +++ b/src/runtime/browser/asn1.ts @@ -60,8 +60,14 @@ const getNamedCurve = (keyData: Uint8Array): string => { return 'P-384' case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x23]): return 'P-521' - case isCloudflareWorkers() && findOid(keyData, [0x2b, 0x65, 0x70]): + case findOid(keyData, [0x2b, 0x65, 0x6e]): + return 'X25519' + case findOid(keyData, [0x2b, 0x65, 0x6f]): + return 'X448' + case findOid(keyData, [0x2b, 0x65, 0x70]): return 'Ed25519' + case findOid(keyData, [0x2b, 0x65, 0x71]): + return 'Ed448' default: throw new JOSENotSupported('Invalid or unsupported EC Key Curve or OKP Key Sub Type') } @@ -123,15 +129,22 @@ const genericImport = async ( case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': - case 'ECDH-ES+A256KW': - algorithm = { name: 'ECDH', namedCurve: getNamedCurve(keyData) } + case 'ECDH-ES+A256KW': { + const namedCurve = getNamedCurve(keyData) + algorithm = namedCurve.startsWith('P-') ? { name: 'ECDH', namedCurve } : { name: namedCurve } keyUsages = isPublic ? [] : ['deriveBits'] break - case isCloudflareWorkers() && 'EdDSA': + } + case isCloudflareWorkers() && 'EdDSA': { const namedCurve = getNamedCurve(keyData).toUpperCase() algorithm = { name: `NODE-${namedCurve}`, namedCurve: `NODE-${namedCurve}` } keyUsages = isPublic ? ['verify'] : ['sign'] break + } + case 'EdDSA': + algorithm = { name: getNamedCurve(keyData) } + keyUsages = isPublic ? ['verify'] : ['sign'] + break default: throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value') } diff --git a/src/runtime/browser/ecdhes.ts b/src/runtime/browser/ecdhes.ts index b3eb85084c..73cc05f3a3 100644 --- a/src/runtime/browser/ecdhes.ts +++ b/src/runtime/browser/ecdhes.ts @@ -28,14 +28,24 @@ export async function deriveKey( uint32be(keyLength), ) + let length: number + if (publicKey.algorithm.name === 'X25519') { + length = 256 + } else if (publicKey.algorithm.name === 'X448') { + length = 448 + } else { + length = + Math.ceil(parseInt((publicKey.algorithm).namedCurve.substr(-3), 10) / 8) << 3 + } + const sharedSecret = new Uint8Array( await crypto.subtle.deriveBits( { - name: 'ECDH', + name: publicKey.algorithm.name, public: publicKey, }, privateKey, - Math.ceil(parseInt((privateKey.algorithm).namedCurve.slice(-3), 10) / 8) << 3, + length, ), ) @@ -54,5 +64,9 @@ export function ecdhAllowed(key: unknown) { if (!isCryptoKey(key)) { throw new TypeError(invalidKeyInput(key, ...types)) } - return ['P-256', 'P-384', 'P-521'].includes((key.algorithm).namedCurve) + return ( + ['P-256', 'P-384', 'P-521'].includes((key.algorithm).namedCurve) || + key.algorithm.name === 'X25519' || + key.algorithm.name === 'X448' + ) } diff --git a/src/runtime/browser/env.ts b/src/runtime/browser/env.ts index d230986d3c..c756022f1b 100644 --- a/src/runtime/browser/env.ts +++ b/src/runtime/browser/env.ts @@ -1,4 +1,10 @@ -export function isCloudflareWorkers(): boolean { - // @ts-expect-error - return typeof WebSocketPair === 'function' +export function isCloudflareWorkers() { + return ( + // @ts-ignore + typeof WebSocketPair !== 'undefined' || + // @ts-ignore + (typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers') || + // @ts-ignore + (typeof EdgeRuntime !== 'undefined' && EdgeRuntime === 'vercel') + ) } diff --git a/src/runtime/browser/generate.ts b/src/runtime/browser/generate.ts index be378c25b9..1b80fb9c3d 100644 --- a/src/runtime/browser/generate.ts +++ b/src/runtime/browser/generate.ts @@ -59,7 +59,7 @@ function getModulusLengthOption(options?: GenerateKeyPairOptions) { } export async function generateKeyPair(alg: string, options?: GenerateKeyPairOptions) { - let algorithm: RsaHashedKeyGenParams | EcKeyGenParams + let algorithm: RsaHashedKeyGenParams | EcKeyGenParams | KeyAlgorithm let keyUsages: KeyUsage[] switch (alg) { @@ -120,13 +120,42 @@ export async function generateKeyPair(alg: string, options?: GenerateKeyPairOpti throw new JOSENotSupported('Invalid or unsupported crv option provided') } break + case 'EdDSA': + keyUsages = ['sign', 'verify'] + const crv = options?.crv ?? 'Ed25519' + switch (crv) { + case 'Ed25519': + case 'Ed448': + algorithm = { name: crv } + break + default: + throw new JOSENotSupported('Invalid or unsupported crv option provided') + } + break case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': - case 'ECDH-ES+A256KW': - algorithm = { name: 'ECDH', namedCurve: options?.crv ?? 'P-256' } + case 'ECDH-ES+A256KW': { keyUsages = ['deriveKey', 'deriveBits'] + const crv = options?.crv ?? 'P-256' + switch (crv) { + case 'P-256': + case 'P-384': + case 'P-521': { + algorithm = { name: 'ECDH', namedCurve: crv } + break + } + case 'X25519': + case 'X448': + algorithm = { name: crv } + break + default: + throw new JOSENotSupported( + 'Invalid or unsupported crv option provided, supported values are P-256, P-384, P-521, X25519, and X448', + ) + } break + } default: throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value') } diff --git a/src/runtime/browser/jwk_to_key.ts b/src/runtime/browser/jwk_to_key.ts index 8ed0f56302..019bbd3179 100644 --- a/src/runtime/browser/jwk_to_key.ts +++ b/src/runtime/browser/jwk_to_key.ts @@ -116,11 +116,27 @@ function subtleMapping(jwk: JWK): { keyUsages = jwk.d ? ['sign'] : ['verify'] break default: - throw new JOSENotSupported( - 'Invalid or unsupported JWK "crv" (Subtype of Key Pair) Parameter value', - ) + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value') } break + case 'OKP': { + switch (jwk.alg) { + case 'EdDSA': + algorithm = { name: jwk.crv! } + keyUsages = jwk.d ? ['sign'] : ['verify'] + break + case 'ECDH-ES': + case 'ECDH-ES+A128KW': + case 'ECDH-ES+A192KW': + case 'ECDH-ES+A256KW': + algorithm = { name: jwk.crv! } + keyUsages = jwk.d ? ['deriveBits'] : [] + break + default: + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value') + } + break + } default: throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value') } diff --git a/src/runtime/browser/subtle_dsa.ts b/src/runtime/browser/subtle_dsa.ts index 471eec0df9..9bf9b061ec 100644 --- a/src/runtime/browser/subtle_dsa.ts +++ b/src/runtime/browser/subtle_dsa.ts @@ -24,6 +24,8 @@ export default function subtleDsa(alg: string, algorithm: KeyAlgorithm | EcKeyAl case isCloudflareWorkers() && 'EdDSA': const { namedCurve } = algorithm return { name: namedCurve, namedCurve } + case 'EdDSA': + return { name: algorithm.name } default: throw new JOSENotSupported( `alg ${alg} is not supported either by JOSE or your javascript runtime`, diff --git a/test/jwe/smoke.test.mjs b/test/jwe/smoke.test.mjs index bcf6cf8e83..69aeeff2e7 100644 --- a/test/jwe/smoke.test.mjs +++ b/test/jwe/smoke.test.mjs @@ -299,12 +299,12 @@ test('as keyobject', smoke, 'oct256gcm', ['encrypt'], ['decrypt'], true) test(smoke, 'oct256c') test(smoke, 'oct384c') test(smoke, 'oct512c') +test(smoke, 'x25519dir') conditional({ webcrypto: 0 })(smoke, 'rsa1_5') -conditional({ webcrypto: 0, electron: 0 })(smoke, 'x25519kw') -conditional({ webcrypto: 0 })(smoke, 'x25519dir') -conditional({ webcrypto: 0, electron: 0 })(smoke, 'x448kw') -conditional({ webcrypto: 0, electron: 0 })(smoke, 'x448dir') +conditional({ electron: 0 })(smoke, 'x25519kw') +conditional({ electron: 0 })(smoke, 'x448kw') +conditional({ electron: 0 })(smoke, 'x448dir') conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct256c', undefined, undefined, true) conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct384c', undefined, undefined, true) conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct512c', undefined, undefined, true) diff --git a/test/jwk/jwk2key.test.mjs b/test/jwk/jwk2key.test.mjs index 166e08b1d0..a980750650 100644 --- a/test/jwk/jwk2key.test.mjs +++ b/test/jwk/jwk2key.test.mjs @@ -173,10 +173,43 @@ const rsa = { qi: 'htPHLViOVG6QrldfuHn9evfdlD-UEuViOWNx8aKR3IBv0qegpJ78vYB4hdAcJZtBslKI97En5rzOAN3Y6Y8MbI4oN77WeiePJl2cMrS64evmlERvjJ6ZTs8jK0iV5q_gIZ9Qg9drmolUgb_CccQOBFbqSL6YkXwCBxlkCrzTlhc', kty: 'RSA', } +const secp256k1 = { + crv: 'secp256k1', + x: 'WsY3Cti12AIuzgUEIINSmyhT8O6-o_6sBaUnjxKtJkE', + y: 'yejzoIE2tLzM_av8Pbd3rW7adTxlUqys2Ajk-JCBLp8', + d: '47Iw2GXvj-hpfgGsfF3F2mekHKaDc2qv7WTqtAkU1H0', + kty: 'EC', +} +const ed25519 = { + crv: 'Ed25519', + x: 'GVLslCt7dY6H8p_yatNaGOtpdrCho5qaLvIvNTMd29M', + d: 'FRaWZohbbDyzhYpTCS9m4fv2xoK6HG83bw6jq6zNxEs', + kty: 'OKP', +} +const ed448 = { + crv: 'Ed448', + x: 'KYWcaDwgH77xdAwcbzOgvCVcGMy9I6prRQBhQTTdKXUcr-VquTz7Fd5adJO0wT2VHysF3bk3kBoA', + d: 'UhC3-vN5vp_g9PnTknXZgfXUez7Xvw-OfuJ0pYkuwzpYkcTvacqoFkV_O05WMHpyXkzH9q2wzx5n', + kty: 'OKP', +} +const x25519 = { + crv: 'X25519', + x: 'axR8Q7PEd74nY9nWaAoAYpMe3gp5sWbau6V6X1inPw4', + d: 'aCvvb3jEBnxJJBjCIN2a9ZDTL-HG6LVgBbij4m8-d3Y', + kty: 'OKP', +} +const x448 = { + crv: 'X448', + x: 'z8s0Ej7D4pgIDu233UHoDW48EbiEm5eFv8_LuFwRr0xVREHhCtdxH75x6J8egZbjDGweOSbeHbY', + d: 'xBrCwLlrHa1ov2cbmD4eMw4t6DoN_MWsBT_mxcA_QWsCS_9sKMRyFpphNN9_2iKrGPTC9pWCS5w', + kty: 'OKP', +} test(testKeyImportExport, { ...rsa, alg: 'RS256' }) test(testKeyImportExport, { ...rsa, alg: 'PS256' }) test(testKeyImportExport, { ...rsa, alg: 'RSA-OAEP' }) test(testKeyImportExport, { ...rsa, alg: 'RSA-OAEP-256' }) +test(testKeyImportExport, { ...ed25519, alg: 'EdDSA' }) +test(testKeyImportExport, { ...x25519, alg: 'ECDH-ES' }) test('Uin8tArray can be transformed to a JWK', async (t) => { t.deepEqual( @@ -203,43 +236,9 @@ conditional({ webcrypto: 0 })('secret key object can be transformed to a JWK', a kty: 'oct', }) }) - -const secp256k1 = { - crv: 'secp256k1', - x: 'WsY3Cti12AIuzgUEIINSmyhT8O6-o_6sBaUnjxKtJkE', - y: 'yejzoIE2tLzM_av8Pbd3rW7adTxlUqys2Ajk-JCBLp8', - d: '47Iw2GXvj-hpfgGsfF3F2mekHKaDc2qv7WTqtAkU1H0', - kty: 'EC', -} conditional({ webcrypto: 0, electron: 0 })(testKeyImportExport, { ...secp256k1, alg: 'ES256K', }) -const ed25519 = { - crv: 'Ed25519', - x: 'GVLslCt7dY6H8p_yatNaGOtpdrCho5qaLvIvNTMd29M', - d: 'FRaWZohbbDyzhYpTCS9m4fv2xoK6HG83bw6jq6zNxEs', - kty: 'OKP', -} -conditional({ webcrypto: 0 })(testKeyImportExport, { ...ed25519, alg: 'EdDSA' }) -const ed448 = { - crv: 'Ed448', - x: 'KYWcaDwgH77xdAwcbzOgvCVcGMy9I6prRQBhQTTdKXUcr-VquTz7Fd5adJO0wT2VHysF3bk3kBoA', - d: 'UhC3-vN5vp_g9PnTknXZgfXUez7Xvw-OfuJ0pYkuwzpYkcTvacqoFkV_O05WMHpyXkzH9q2wzx5n', - kty: 'OKP', -} -conditional({ webcrypto: 0, electron: 0 })(testKeyImportExport, { ...ed448, alg: 'EdDSA' }) -const x25519 = { - crv: 'X25519', - x: 'axR8Q7PEd74nY9nWaAoAYpMe3gp5sWbau6V6X1inPw4', - d: 'aCvvb3jEBnxJJBjCIN2a9ZDTL-HG6LVgBbij4m8-d3Y', - kty: 'OKP', -} -conditional({ webcrypto: 0 })(testKeyImportExport, { ...x25519, alg: 'ECDH-ES' }) -const x448 = { - crv: 'X448', - x: 'z8s0Ej7D4pgIDu233UHoDW48EbiEm5eFv8_LuFwRr0xVREHhCtdxH75x6J8egZbjDGweOSbeHbY', - d: 'xBrCwLlrHa1ov2cbmD4eMw4t6DoN_MWsBT_mxcA_QWsCS_9sKMRyFpphNN9_2iKrGPTC9pWCS5w', - kty: 'OKP', -} -conditional({ webcrypto: 0, electron: 0 })(testKeyImportExport, { ...x448, alg: 'ECDH-ES' }) +conditional({ electron: 0 })(testKeyImportExport, { ...ed448, alg: 'EdDSA' }) +conditional({ electron: 0 })(testKeyImportExport, { ...x448, alg: 'ECDH-ES' }) diff --git a/test/jws/cookbook.test.mjs b/test/jws/cookbook.test.mjs index d4336b9ee0..987d1c884c 100644 --- a/test/jws/cookbook.test.mjs +++ b/test/jws/cookbook.test.mjs @@ -146,7 +146,7 @@ const vectors = [ }, { title: 'https://www.rfc-editor.org/rfc/rfc8037#appendix-A.4 - Ed25519 Signing', - webcrypto: false, + webcrypto: true, reproducible: true, input: { payload: 'Example of Ed25519 signing', diff --git a/test/jws/smoke.test.mjs b/test/jws/smoke.test.mjs index 9115a8d36e..03c6f4b65b 100644 --- a/test/jws/smoke.test.mjs +++ b/test/jws/smoke.test.mjs @@ -193,10 +193,10 @@ test(smoke, 'p521') test(smoke, 'oct256') test(smoke, 'oct384') test(smoke, 'oct512') +test(smoke, 'ed25519') test('as keyobject', smoke, 'oct256', true) test('as keyobject', smoke, 'oct384', true) test('as keyobject', smoke, 'oct512', true) conditional({ webcrypto: 0, electron: 0 })(smoke, 'secp256k1') -conditional({ webcrypto: 0 })(smoke, 'ed25519') -conditional({ webcrypto: 0, electron: 0 })(smoke, 'ed448') +conditional({ electron: 0 })(smoke, 'ed448') diff --git a/test/key/importexport.test.mjs b/test/key/importexport.test.mjs index 830f41dc45..a49704f64b 100644 --- a/test/key/importexport.test.mjs +++ b/test/key/importexport.test.mjs @@ -156,31 +156,11 @@ for (const alg of ['ES256K']) { } for (const alg of ['EdDSA']) { - conditional({ webcrypto: 0 })( - `import SPKI ed25519 for ${alg}`, - testSPKI, - keys.ed25519.publicKey, - alg, - ) - conditional({ webcrypto: 0 })( - `import X509 ed25519 for ${alg}`, - testX509, - keys.ed25519.certificate, - alg, - ) - conditional({ webcrypto: 0 })( - `import PKCS8 ed25519 for ${alg}`, - testPKCS8, - keys.ed25519.privateKey, - alg, - ) - conditional({ webcrypto: 0, electron: 0 })( - `import SPKI ed448 for ${alg}`, - testSPKI, - keys.ed448.publicKey, - alg, - ) - conditional({ webcrypto: 0, electron: 0 })( + test(`import SPKI ed25519 for ${alg}`, testSPKI, keys.ed25519.publicKey, alg) + test(`import X509 ed25519 for ${alg}`, testX509, keys.ed25519.certificate, alg) + test(`import PKCS8 ed25519 for ${alg}`, testPKCS8, keys.ed25519.privateKey, alg) + conditional({ electron: 0 })(`import SPKI ed448 for ${alg}`, testSPKI, keys.ed448.publicKey, alg) + conditional({ electron: 0 })( `import PKCS8 ed448 for ${alg}`, testPKCS8, keys.ed448.privateKey, @@ -189,28 +169,8 @@ for (const alg of ['EdDSA']) { } for (const alg of ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) { - conditional({ webcrypto: 0, electron: 1 })( - `import SPKI x25519 for ${alg}`, - testSPKI, - keys.x25519.publicKey, - alg, - ) - conditional({ webcrypto: 0, electron: 1 })( - `import PKCS8 x25519 for ${alg}`, - testPKCS8, - keys.x25519.privateKey, - alg, - ) - conditional({ webcrypto: 0, electron: 0 })( - `import SPKI x448 for ${alg}`, - testSPKI, - keys.x448.publicKey, - alg, - ) - conditional({ webcrypto: 0, electron: 0 })( - `import PKCS8 x448 for ${alg}`, - testPKCS8, - keys.x448.privateKey, - alg, - ) + test(`import SPKI x25519 for ${alg}`, testSPKI, keys.x25519.publicKey, alg) + test(`import PKCS8 x25519 for ${alg}`, testPKCS8, keys.x25519.privateKey, alg) + conditional({ electron: 0 })(`import SPKI x448 for ${alg}`, testSPKI, keys.x448.publicKey, alg) + conditional({ electron: 0 })(`import PKCS8 x448 for ${alg}`, testPKCS8, keys.x448.privateKey, alg) } diff --git a/test/util/generators.test.mjs b/test/util/generators.test.mjs index b9160a1874..35735e9055 100644 --- a/test/util/generators.test.mjs +++ b/test/util/generators.test.mjs @@ -132,38 +132,21 @@ if ('WEBCRYPTO' in process.env || 'CRYPTOKEY' in process.env) { test('with extractable: true', testKeyPair, 'PS256', { extractable: true }) } -conditional({ webcrypto: 0 })(testKeyPair, 'EdDSA') -conditional({ webcrypto: 0 })('crv: Ed25519', testKeyPair, 'EdDSA', { crv: 'Ed25519' }) -conditional({ webcrypto: 0, electron: 0 })('crv: Ed448', testKeyPair, 'EdDSA', { - crv: 'Ed448', -}) +test(testKeyPair, 'EdDSA') +test('crv: Ed25519', testKeyPair, 'EdDSA', { crv: 'Ed25519' }) +conditional({ electron: 0 })('crv: Ed448', testKeyPair, 'EdDSA', { crv: 'Ed448' }) conditional({ webcrypto: 0, electron: 0 })(testKeyPair, 'ES256K') conditional({ webcrypto: 0 })(testKeyPair, 'RSA1_5') conditional({ webcrypto: 0 })('with modulusLength', testKeyPair, 'RSA1_5', { modulusLength: 4096, }) for (const crv of ['X25519', 'X448']) { - conditional({ webcrypto: 0, electron: crv === 'X25519' })(`crv: ${crv}`, testKeyPair, 'ECDH-ES', { + conditional({ electron: crv === 'X25519' })(`crv: ${crv}`, testKeyPair, 'ECDH-ES', { crv, }) - conditional({ webcrypto: 0, electron: crv === 'X25519' })( - `crv: ${crv}`, - testKeyPair, - 'ECDH-ES+A128KW', - { crv }, - ) - conditional({ webcrypto: 0, electron: crv === 'X25519' })( - `crv: ${crv}`, - testKeyPair, - 'ECDH-ES+A192KW', - { crv }, - ) - conditional({ webcrypto: 0, electron: crv === 'X25519' })( - `crv: ${crv}`, - testKeyPair, - 'ECDH-ES+A256KW', - { crv }, - ) + conditional({ electron: crv === 'X25519' })(`crv: ${crv}`, testKeyPair, 'ECDH-ES+A128KW', { crv }) + conditional({ electron: crv === 'X25519' })(`crv: ${crv}`, testKeyPair, 'ECDH-ES+A192KW', { crv }) + conditional({ electron: crv === 'X25519' })(`crv: ${crv}`, testKeyPair, 'ECDH-ES+A256KW', { crv }) } async function testSecret(t, alg, expectedLength, options) {