diff --git a/README.md b/README.md index b7f0abe6af..ab5a65452f 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Legend: | -- | -- | -- | | Node.js | LTS ^12.19.0 || ^14.15.0 | | | Electron | ^12.0.0 | see [1] | -| Cloudflare Workers | ✓ | see [2], [4] | +| Cloudflare Workers | ✓ | see [2], [5] | | Deno | experimental | see Deno's [Web Cryptography API roadmap](https://github.com/denoland/deno/issues/11690) | | React Native | ✕ | has no available and usable crypto runtime | | IE | ✕ | implements old version of the Web Cryptography API specification | @@ -163,6 +163,8 @@ Legend: 4 192 bit AES keys are not supported in Chromium +5 OKP / EdDSA / Ed25519 is supported + ## FAQ #### Supported Versions diff --git a/src/runtime/browser/fetch_jwks.ts b/src/runtime/browser/fetch_jwks.ts index 8aa8ce0c39..80b60c01d7 100644 --- a/src/runtime/browser/fetch_jwks.ts +++ b/src/runtime/browser/fetch_jwks.ts @@ -16,7 +16,7 @@ const fetchJwks: FetchFunction = async (url: URL, timeout: number) => { // do not pass referrerPolicy, credentials, and mode when running // in Cloudflare Workers environment // @ts-expect-error - ...(typeof globalThis.WebSocketPair === 'undefined' + ...(globalThis.WebSocketPair === undefined ? { referrerPolicy: 'no-referrer', credentials: 'omit', diff --git a/src/runtime/browser/generate.ts b/src/runtime/browser/generate.ts index 3d9ed05f59..f17f1ec04c 100644 --- a/src/runtime/browser/generate.ts +++ b/src/runtime/browser/generate.ts @@ -1,3 +1,4 @@ +import { isCloudflareWorkers, isNodeJs } from './global.js' import crypto from './webcrypto.js' import { JOSENotSupported } from '../../util/errors.js' import random from './random.js' @@ -109,6 +110,23 @@ export async function generateKeyPair(alg: string, options?: GenerateKeyPairOpti algorithm = { name: 'ECDSA', namedCurve: 'P-521' } keyUsages = ['sign', 'verify'] break + case (isCloudflareWorkers() || isNodeJs()) && 'EdDSA': + switch (options?.crv) { + case undefined: + case 'Ed25519': + algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' } + keyUsages = ['sign', 'verify'] + break + case isNodeJs() && 'Ed448': + algorithm = { name: 'NODE-ED448', namedCurve: 'NODE-ED448' } + keyUsages = ['sign', 'verify'] + break + default: + throw new JOSENotSupported( + 'Invalid or unsupported crv option provided, supported values are Ed25519 and Ed448', + ) + } + break case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': diff --git a/src/runtime/browser/global.ts b/src/runtime/browser/global.ts index ba85627857..b0b3718c08 100644 --- a/src/runtime/browser/global.ts +++ b/src/runtime/browser/global.ts @@ -6,3 +6,19 @@ function getGlobal() { } export default getGlobal() +export function isCloudflareWorkers(): boolean { + try { + // @ts-expect-error + return getGlobal().WebSocketPair !== undefined + } catch { + return false + } +} +export function isNodeJs(): boolean { + try { + // @deno-expect-error + return getGlobal().process?.versions?.node !== undefined + } catch { + return false + } +} diff --git a/src/runtime/browser/jwk_to_key.ts b/src/runtime/browser/jwk_to_key.ts index 770904a063..f28df56784 100644 --- a/src/runtime/browser/jwk_to_key.ts +++ b/src/runtime/browser/jwk_to_key.ts @@ -1,3 +1,4 @@ +import { isCloudflareWorkers, isNodeJs } from './global.js' import crypto from './webcrypto.js' import type { JWKParseFunction } from '../interfaces.d' import { JOSENotSupported } from '../../util/errors.js' @@ -84,9 +85,15 @@ function subtleMapping(jwk: JWK): { case 'EC': { switch (jwk.alg) { case 'ES256': + algorithm = { name: 'ECDSA', namedCurve: 'P-256' } + keyUsages = jwk.d ? ['sign'] : ['verify'] + break case 'ES384': + algorithm = { name: 'ECDSA', namedCurve: 'P-384' } + keyUsages = jwk.d ? ['sign'] : ['verify'] + break case 'ES512': - algorithm = { name: 'ECDSA', namedCurve: jwk.crv! } + algorithm = { name: 'ECDSA', namedCurve: 'P-521' } keyUsages = jwk.d ? ['sign'] : ['verify'] break case 'ECDH-ES': @@ -101,6 +108,25 @@ function subtleMapping(jwk: JWK): { } break } + case (isCloudflareWorkers() || isNodeJs()) && 'OKP': + if (jwk.alg !== 'EdDSA') { + throw new JOSENotSupported('unsupported or invalid JWK "alg" (Algorithm) Parameter value') + } + switch (jwk.crv) { + case 'Ed25519': + algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' } + keyUsages = jwk.d ? ['sign'] : ['verify'] + break + case isNodeJs() && 'Ed448': + algorithm = { name: 'NODE-ED448', namedCurve: 'NODE-ED448' } + keyUsages = jwk.d ? ['sign'] : ['verify'] + break + default: + throw new JOSENotSupported( + 'unsupported or invalid JWK "crv" (Subtype of Key Pair) Parameter value', + ) + } + break default: throw new JOSENotSupported('unsupported or invalid JWK "kty" (Key Type) Parameter value') } diff --git a/src/runtime/browser/sign.ts b/src/runtime/browser/sign.ts index f1535d5b38..ace47a79f1 100644 --- a/src/runtime/browser/sign.ts +++ b/src/runtime/browser/sign.ts @@ -7,7 +7,12 @@ import getSignKey from './get_sign_verify_key.js' const sign: SignFunction = async (alg, key: unknown, data) => { const cryptoKey = await getSignKey(alg, key, 'sign') checkKeyLength(alg, cryptoKey) - const signature = await crypto.subtle.sign(subtleAlgorithm(alg), cryptoKey, data) + const signature = await crypto.subtle.sign( + // @deno-expect-error + subtleAlgorithm(alg, (cryptoKey.algorithm).namedCurve), + cryptoKey, + data, + ) return new Uint8Array(signature) } diff --git a/src/runtime/browser/subtle_dsa.ts b/src/runtime/browser/subtle_dsa.ts index d789c9b7a3..e86359a508 100644 --- a/src/runtime/browser/subtle_dsa.ts +++ b/src/runtime/browser/subtle_dsa.ts @@ -1,6 +1,7 @@ +import { isCloudflareWorkers, isNodeJs } from './global.js' import { JOSENotSupported } from '../../util/errors.js' -export default function subtleDsa(alg: string) { +export default function subtleDsa(alg: string, crv?: string) { switch (alg) { case 'HS256': return { hash: { name: 'SHA-256' }, name: 'HMAC' } @@ -38,6 +39,9 @@ export default function subtleDsa(alg: string) { return { hash: { name: 'SHA-384' }, name: 'ECDSA', namedCurve: 'P-384' } case 'ES512': return { hash: { name: 'SHA-512' }, name: 'ECDSA', namedCurve: 'P-521' } + case (isCloudflareWorkers() || isNodeJs()) && 'EdDSA': + // @deno-expect-error + return { name: crv, namedCurve: crv } default: throw new JOSENotSupported( `alg ${alg} is not supported either by JOSE or your javascript runtime`, diff --git a/src/runtime/browser/verify.ts b/src/runtime/browser/verify.ts index a7f5892bfc..ad643efc81 100644 --- a/src/runtime/browser/verify.ts +++ b/src/runtime/browser/verify.ts @@ -7,7 +7,8 @@ import getVerifyKey from './get_sign_verify_key.js' const verify: VerifyFunction = async (alg, key: unknown, signature, data) => { const cryptoKey = await getVerifyKey(alg, key, 'verify') checkKeyLength(alg, cryptoKey) - const algorithm = subtleAlgorithm(alg) + // @deno-expect-error + const algorithm = subtleAlgorithm(alg, (cryptoKey.algorithm).namedCurve) try { return await crypto.subtle.verify(algorithm, cryptoKey, signature, data) } catch { diff --git a/src/runtime/node/webcrypto.ts b/src/runtime/node/webcrypto.ts index a6d28ff37a..52b4f9ca0e 100644 --- a/src/runtime/node/webcrypto.ts +++ b/src/runtime/node/webcrypto.ts @@ -84,6 +84,14 @@ export function getKeyObject(key: CryptoKey, alg?: string, usage?: Set } break } + case 'EdDSA': { + if (key.algorithm.name !== 'NODE-ED25519' && key.algorithm.name !== 'NODE-ED448') { + throw new TypeError( + `CryptoKey does not support this operation, its algorithm.name must be NODE-ED25519 or NODE-ED448.`, + ) + } + break + } case 'ES256': case 'ES384': case 'ES512': { diff --git a/test-cloudflare-workers/cloudflare.test.mjs b/test-cloudflare-workers/cloudflare.test.mjs index 9a7df08209..c7a93650bd 100644 --- a/test-cloudflare-workers/cloudflare.test.mjs +++ b/test-cloudflare-workers/cloudflare.test.mjs @@ -164,6 +164,18 @@ test('ES512', macro, async () => { await jwsAsymmetricTest(keypair, alg); }); +test('EdDSA', macro, async () => { + const alg = 'EdDSA'; + const keypair = await utilGenerateKeyPair(alg); + await jwsAsymmetricTest(keypair, alg); +}); + +test('EdDSA crv: Ed25519', macro, async () => { + const alg = 'EdDSA'; + const keypair = await utilGenerateKeyPair(alg, { crv: 'Ed25519' }); + await jwsAsymmetricTest(keypair, alg); +}); + test('createRemoteJWKSet', macro, async () => { const jwksUri = 'https://www.googleapis.com/oauth2/v3/certs'; const response = await fetch(jwksUri).then((r) => r.json()); diff --git a/test/jwk/jwk2key.test.mjs b/test/jwk/jwk2key.test.mjs index f394bca42e..0759c7075e 100644 --- a/test/jwk/jwk2key.test.mjs +++ b/test/jwk/jwk2key.test.mjs @@ -244,14 +244,14 @@ Promise.all([import(`${keyRoot}/jwk/parse`), import(`${keyRoot}/jwk/from_key_lik d: 'FRaWZohbbDyzhYpTCS9m4fv2xoK6HG83bw6jq6zNxEs', kty: 'OKP', }; - conditional({ webcrypto: 0 })(testKeyImportExport, { ...ed25519, alg: 'EdDSA' }); + conditional({ webcrypto: 1 })(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' }); + conditional({ webcrypto: 1, electron: 0 })(testKeyImportExport, { ...ed448, alg: 'EdDSA' }); const x25519 = { crv: 'X25519', x: 'axR8Q7PEd74nY9nWaAoAYpMe3gp5sWbau6V6X1inPw4', diff --git a/test/jws/cookbook.test.mjs b/test/jws/cookbook.test.mjs index 2ebec177e9..97be89053d 100644 --- a/test/jws/cookbook.test.mjs +++ b/test/jws/cookbook.test.mjs @@ -169,7 +169,7 @@ Promise.all([ }, { title: 'https://tools.ietf.org/html/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 9f50043731..016dd23c2f 100644 --- a/test/jws/smoke.test.mjs +++ b/test/jws/smoke.test.mjs @@ -219,8 +219,8 @@ Promise.all([ } conditional({ webcrypto: 0, electron: 0 })(smoke, 'secp256k1'); - conditional({ webcrypto: 0 })(smoke, 'ed25519'); - conditional({ webcrypto: 0, electron: 0 })(smoke, 'ed448'); + conditional({ webcrypto: 1 })(smoke, 'ed25519'); + conditional({ webcrypto: 1, electron: 0 })(smoke, 'ed448'); }, (err) => { test('failed to import', (t) => { diff --git a/test/util/generators.test.mjs b/test/util/generators.test.mjs index a29cd9560f..403e35beef 100644 --- a/test/util/generators.test.mjs +++ b/test/util/generators.test.mjs @@ -158,9 +158,9 @@ Promise.all([ return run; } - conditional({ webcrypto: 0 })(testKeyPair, 'EdDSA'); - conditional({ webcrypto: 0 })('crv: Ed25519', testKeyPair, 'EdDSA', { crv: 'Ed25519' }); - conditional({ webcrypto: 0, electron: 0 })('crv: Ed448', testKeyPair, 'EdDSA', { + conditional({ webcrypto: 1 })(testKeyPair, 'EdDSA'); + conditional({ webcrypto: 1 })('crv: Ed25519', testKeyPair, 'EdDSA', { crv: 'Ed25519' }); + conditional({ webcrypto: 1, electron: 0 })('crv: Ed448', testKeyPair, 'EdDSA', { crv: 'Ed448', }); conditional({ webcrypto: 0, electron: 0 })(testKeyPair, 'ES256K');